From 3e112fb1acc927c65517ce95961b74db9cc336f2 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Thu, 10 Dec 2020 09:37:06 -0500 Subject: [PATCH 01/51] internal/oidc/dynamiccodec: first draft Note that we don't cache the securecookie.SecureCookie that we use in our implementation. This was purely because of laziness. We should think about caching this value in the future. Signed-off-by: Andrew Keesler --- internal/oidc/dynamiccodec/codec.go | 42 +++++++++ internal/oidc/dynamiccodec/codec_test.go | 110 +++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 internal/oidc/dynamiccodec/codec.go create mode 100644 internal/oidc/dynamiccodec/codec_test.go diff --git a/internal/oidc/dynamiccodec/codec.go b/internal/oidc/dynamiccodec/codec.go new file mode 100644 index 00000000..0517e11c --- /dev/null +++ b/internal/oidc/dynamiccodec/codec.go @@ -0,0 +1,42 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package dynamiccodec provides a type that can encode information using a just-in-time signing and +// (optionally) encryption secret. +package dynamiccodec + +import ( + "github.com/gorilla/securecookie" + + "go.pinniped.dev/internal/oidc" +) + +var _ oidc.Codec = &Codec{} + +// KeyFunc returns 2 keys: a required signing key, and an optional encryption key. +type KeyFunc func() ([]byte, []byte) + +// Codec can dynamically encode and decode information by using a KeyFunc to get its keys +// just-in-time. +type Codec struct { + keyFunc KeyFunc +} + +// New creates a new Codec that will use the provided keyFunc for its key source. +func New(keyFunc KeyFunc) *Codec { + return &Codec{ + keyFunc: keyFunc, + } +} + +// Encode implements oidc.Encode(). +func (c *Codec) Encode(name string, value interface{}) (string, error) { + signingKey, encryptionKey := c.keyFunc() + return securecookie.New(signingKey, encryptionKey).Encode(name, value) +} + +// Decode implements oidc.Decode(). +func (c *Codec) Decode(name string, value string, into interface{}) error { + signingKey, encryptionKey := c.keyFunc() + return securecookie.New(signingKey, encryptionKey).Decode(name, value, into) +} diff --git a/internal/oidc/dynamiccodec/codec_test.go b/internal/oidc/dynamiccodec/codec_test.go new file mode 100644 index 00000000..7513954a --- /dev/null +++ b/internal/oidc/dynamiccodec/codec_test.go @@ -0,0 +1,110 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package dynamiccodec + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCodec(t *testing.T) { + tests := []struct { + name string + keys func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) + wantEncoderError string + wantDecoderError string + }{ + { + name: "good signing and encryption keys", + }, + { + name: "good signing keys and no encryption key", + keys: func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) { + *encoderEncryptionKey = nil + *decoderEncryptionKey = nil + }, + }, + { + name: "good signing keys and bad encoding encryption key", + keys: func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) { + *encoderEncryptionKey = []byte("this-secret-is-not-16-bytes") + }, + wantEncoderError: "securecookie: error - caused by: crypto/aes: invalid key size 27", + }, + { + name: "good signing keys and bad decoding encryption key", + keys: func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) { + *decoderEncryptionKey = []byte("this-secret-is-not-16-bytes") + }, + wantDecoderError: "securecookie: error - caused by: crypto/aes: invalid key size 27", + }, + { + name: "bad encoder signing key", + keys: func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) { + *encoderSigningKey = nil + }, + wantEncoderError: "securecookie: hash key is not set", + }, + { + name: "bad decoder signing key", + keys: func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) { + *decoderSigningKey = nil + }, + wantDecoderError: "securecookie: hash key is not set", + }, + { + name: "signing key mismatch", + keys: func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) { + *encoderSigningKey = []byte("this key does not match the decoder key") + }, + wantDecoderError: "securecookie: the value is not valid", + }, + { + name: "encryption key mismatch", + keys: func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) { + *encoderEncryptionKey = []byte("16-byte-no-match") + }, + wantDecoderError: "securecookie: error - caused by: securecookie: error - caused by: gob: encoded unsigned integer out of range", + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + var ( + encoderSigningKey = []byte("some-signing-key") + encoderEncryptionKey = []byte("16-byte-encr-key") + decoderSigningKey = []byte("some-signing-key") + decoderEncryptionKey = []byte("16-byte-encr-key") + ) + if test.keys != nil { + test.keys(&encoderSigningKey, &encoderEncryptionKey, &decoderSigningKey, &decoderEncryptionKey) + } + encoder := New(func() ([]byte, []byte) { + return encoderSigningKey, encoderEncryptionKey + }) + + encoded, err := encoder.Encode("some-name", "some-message") + if test.wantEncoderError != "" { + require.EqualError(t, err, test.wantEncoderError) + return + } + require.NoError(t, err) + + decoder := New(func() ([]byte, []byte) { + return decoderSigningKey, decoderEncryptionKey + }) + + var decoded string + err = decoder.Decode("some-name", encoded, &decoded) + if test.wantDecoderError != "" { + require.EqualError(t, err, test.wantDecoderError) + return + } + require.NoError(t, err) + + require.Equal(t, "some-message", decoded) + }) + } +} From c3f73ffb57310e402ed7d5f9bc1fae25ad89d13d Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Thu, 10 Dec 2020 11:54:36 -0500 Subject: [PATCH 02/51] Check in some musings on a symmetric key generator controller There is still a test failing, but I am sure it is a simple fix hiding in the code. I think this is the general shape of the controller that we want. Signed-off-by: Andrew Keesler --- go.mod | 1 + .../secretgenerator/secretgenerator.go | 166 ++++++++++ .../secretgenerator/secretgenerator_test.go | 294 ++++++++++++++++++ 3 files changed, 461 insertions(+) create mode 100644 internal/controller/supervisorconfig/secretgenerator/secretgenerator.go create mode 100644 internal/controller/supervisorconfig/secretgenerator/secretgenerator_test.go diff --git a/go.mod b/go.mod index 84237fd7..3abd0ba1 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/ory/fosite v0.35.1 github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/pkg/errors v0.9.1 + github.com/prometheus/common v0.10.0 github.com/sclevine/agouti v3.0.0+incompatible github.com/sclevine/spec v1.4.0 github.com/spf13/cobra v1.0.0 diff --git a/internal/controller/supervisorconfig/secretgenerator/secretgenerator.go b/internal/controller/supervisorconfig/secretgenerator/secretgenerator.go new file mode 100644 index 00000000..89df14e1 --- /dev/null +++ b/internal/controller/supervisorconfig/secretgenerator/secretgenerator.go @@ -0,0 +1,166 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package secretgenerator provides a controller that can ensure existence of a generated secret. +package secretgenerator + +import ( + "context" + "crypto/rand" + "fmt" + + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1informers "k8s.io/client-go/informers/core/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/util/retry" + "k8s.io/klog/v2" + + pinnipedcontroller "go.pinniped.dev/internal/controller" + "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/plog" +) + +const ( + symmetricKeySecretType = "secrets.pinniped.dev/symmetric" + symmetricKeySecretDataKey = "key" + + symmetricKeySize = 32 // TODO: what should this be? +) + +// generateKey is stubbed out for the purpose of testing. The default behavior is to generate a symmetric key. +//nolint:gochecknoglobals +var generateKey = generateSymmetricKey + +func generateSymmetricKey() ([]byte, error) { + b := make([]byte, symmetricKeySize) + if _, err := rand.Read(b); err != nil { + return nil, err + } + return b, nil +} + +type controller struct { + secretNamePrefix string + client kubernetes.Interface + secrets corev1informers.SecretInformer +} + +// New instantiates a new controllerlib.Controller which will ensure existence of a generated secret. +func New(secretNamePrefix string, client kubernetes.Interface, secrets corev1informers.SecretInformer) controllerlib.Controller { + c := controller{ + secretNamePrefix: secretNamePrefix, + client: client, + secrets: secrets, + } + filter := pinnipedcontroller.SimpleFilterWithSingletonQueue(isOwnee) + return controllerlib.New( + controllerlib.Config{Name: secretNamePrefix + "-secrets-generator", Syncer: &c}, + controllerlib.WithInformer(secrets, filter, controllerlib.InformerOption{}), + ) +} + +// Sync implements controllerlib.Syncer.Sync(). +func (c *controller) Sync(ctx controllerlib.Context) error { + secret, err := c.secrets.Lister().Secrets(ctx.Key.Namespace).Get(ctx.Key.Name) + isNotFound := k8serrors.IsNotFound(err) + if !isNotFound && err != nil { + return fmt.Errorf("failed to list secret %s/%s: %w", ctx.Key.Namespace, ctx.Key.Name, err) + } + + secretNeedsUpdate := isNotFound || !c.isValid(secret) + if !secretNeedsUpdate { + plog.Debug("secret is up to date", "secret", klog.KObj(secret)) + return nil + } + + newSecret, err := c.generateSecret(ctx.Key.Namespace) + if err != nil { + return fmt.Errorf("failed to generate secret: %w", err) + } + + if isNotFound { + err = c.createSecret(ctx.Context, newSecret) + } else { + err = c.updateSecret(ctx.Context, newSecret, ctx.Key.Name) + } + if err != nil { + return fmt.Errorf("failed to create/update secret %s/%s: %w", ctx.Key.Namespace, ctx.Key.Name, err) + } + + return nil +} + +func (c *controller) isValid(secret *corev1.Secret) bool { + if secret.Type != symmetricKeySecretType { + return false + } + + data, ok := secret.Data[symmetricKeySecretDataKey] + if !ok { + return false + } + if len(data) != symmetricKeySize { + return false + } + + return true +} + +func (c *controller) generateSecret(namespace string) (*corev1.Secret, error) { + symmetricKey, err := generateKey() + if err != nil { + return nil, err + } + + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: c.secretNamePrefix, + Namespace: namespace, + }, + Type: symmetricKeySecretType, + Data: map[string][]byte{ + symmetricKeySecretDataKey: symmetricKey, + }, + }, nil +} + +func (c *controller) createSecret(ctx context.Context, newSecret *corev1.Secret) error { + _, err := c.client.CoreV1().Secrets(newSecret.Namespace).Create(ctx, newSecret, metav1.CreateOptions{}) + return err +} + +func (c *controller) updateSecret(ctx context.Context, newSecret *corev1.Secret, secretName string) error { + secrets := c.client.CoreV1().Secrets(newSecret.Namespace) + return retry.RetryOnConflict(retry.DefaultBackoff, func() error { + currentSecret, err := secrets.Get(ctx, secretName, metav1.GetOptions{}) + isNotFound := k8serrors.IsNotFound(err) + if !isNotFound && err != nil { + return fmt.Errorf("failed to get secret: %w", err) + } + + if isNotFound { + if err := c.createSecret(ctx, newSecret); err != nil { + return fmt.Errorf("failed to create secret: %w", err) + } + return nil + } + + if c.isValid(currentSecret) { + return nil + } + + currentSecret.Type = newSecret.Type + currentSecret.Data = newSecret.Data + + _, err = secrets.Update(ctx, currentSecret, metav1.UpdateOptions{}) + return err + }) +} + +// isOwnee returns whether the provided obj is owned by this controller. +func isOwnee(obj metav1.Object) bool { + // TODO: how do we say we are owned by our Deployment? + return true +} diff --git a/internal/controller/supervisorconfig/secretgenerator/secretgenerator_test.go b/internal/controller/supervisorconfig/secretgenerator/secretgenerator_test.go new file mode 100644 index 00000000..f1e5a3de --- /dev/null +++ b/internal/controller/supervisorconfig/secretgenerator/secretgenerator_test.go @@ -0,0 +1,294 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package secretgenerator + +import ( + "context" + "errors" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + kubeinformers "k8s.io/client-go/informers" + kubernetesfake "k8s.io/client-go/kubernetes/fake" + kubetesting "k8s.io/client-go/testing" + + "go.pinniped.dev/internal/controllerlib" +) + +func TestController(t *testing.T) { + const ( + generatedSecretNamespace = "some-namespace" + generatedSecretNamePrefix = "some-name-" + generatedSecretName = "some-name-abc123" + ) + + var ( + secretsGVR = schema.GroupVersionResource{ + Group: corev1.SchemeGroupVersion.Group, + Version: corev1.SchemeGroupVersion.Version, + Resource: "secrets", + } + + generatedSymmetricKey = []byte("some-neato-32-byte-generated-key") + + generatedSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: generatedSecretNamePrefix, + Namespace: generatedSecretNamespace, + }, + Type: "secrets.pinniped.dev/symmetric", + Data: map[string][]byte{ + "key": generatedSymmetricKey, + }, + } + ) + + generatedSecretWithName := generatedSecret.DeepCopy() + generatedSecretWithName.Name = generatedSecretName + + once := sync.Once{} + + tests := []struct { + name string + storedSecret func(**corev1.Secret) + generateKey func() ([]byte, error) + apiClient func(*testing.T, *kubernetesfake.Clientset) + wantError string + wantActions []kubetesting.Action + }{ + { + name: "when the secrets does not exist, it gets generated", + storedSecret: func(secret **corev1.Secret) { + *secret = nil + }, + wantActions: []kubetesting.Action{ + kubetesting.NewCreateAction(secretsGVR, generatedSecretNamespace, generatedSecret), + }, + }, + { + name: "when a valid secret exists, nothing happens", + }, + { + name: "secret gets updated when the type is wrong", + storedSecret: func(secret **corev1.Secret) { + (*secret).Type = "wrong" + }, + wantActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), + kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName), + }, + }, + { + name: "secret gets updated when the key data does not exist", + storedSecret: func(secret **corev1.Secret) { + delete((*secret).Data, "key") + }, + wantActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), + kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName), + }, + }, + { + name: "secret gets updated when the key data is too short", + storedSecret: func(secret **corev1.Secret) { + (*secret).Data["key"] = []byte("too short") + }, + wantActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), + kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName), + }, + }, + { + name: "an error is returned when creating fails", + storedSecret: func(secret **corev1.Secret) { + *secret = nil + }, + apiClient: func(t *testing.T, client *kubernetesfake.Clientset) { + client.PrependReactor("create", "secrets", func(action kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("some create error") + }) + }, + wantActions: []kubetesting.Action{ + kubetesting.NewCreateAction(secretsGVR, generatedSecretNamespace, generatedSecret), + }, + wantError: "failed to create/update secret some-namespace/some-name-abc123: some create error", + }, + { + name: "an error is returned when updating fails", + storedSecret: func(secret **corev1.Secret) { + (*secret).Data["key"] = []byte("too short") // force updating + }, + apiClient: func(t *testing.T, client *kubernetesfake.Clientset) { + client.PrependReactor("update", "secrets", func(action kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("some update error") + }) + }, + wantActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), + kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName), + }, + wantError: "failed to create/update secret some-namespace/some-name-abc123: some update error", + }, + { + name: "an error is returned when getting fails", + storedSecret: func(secret **corev1.Secret) { + (*secret).Data["key"] = []byte("too short") // force updating + }, + apiClient: func(t *testing.T, client *kubernetesfake.Clientset) { + client.PrependReactor("get", "secrets", func(action kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("some get error") + }) + }, + wantActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), + }, + wantError: "failed to create/update secret some-namespace/some-name-abc123: failed to get secret: some get error", + }, + { + name: "the update is retried when it fails due to a conflict", + storedSecret: func(secret **corev1.Secret) { + (*secret).Data["key"] = []byte("too short") // force updating + }, + apiClient: func(t *testing.T, client *kubernetesfake.Clientset) { + client.PrependReactor("update", "secrets", func(action kubetesting.Action) (bool, runtime.Object, error) { + var err error + once.Do(func() { + err = k8serrors.NewConflict(secretsGVR.GroupResource(), generatedSecretName, errors.New("some error")) + }) + return true, nil, err + }) + }, + wantActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), + kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName), + kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), + kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName), + }, + }, + { + name: "upon updating we discover that a valid secret exists", + storedSecret: func(secret **corev1.Secret) { + (*secret).Data["key"] = []byte("too short") // force updating + }, + apiClient: func(t *testing.T, client *kubernetesfake.Clientset) { + client.PrependReactor("get", "secrets", func(action kubetesting.Action) (bool, runtime.Object, error) { + return true, generatedSecretWithName, nil + }) + }, + wantActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), + }, + }, + { + name: "upon updating we discover that the secret has been deleted", + storedSecret: func(secret **corev1.Secret) { + (*secret).Data["key"] = []byte("too short") // force updating + }, + apiClient: func(t *testing.T, client *kubernetesfake.Clientset) { + client.PrependReactor("get", "secrets", func(action kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, k8serrors.NewNotFound(secretsGVR.GroupResource(), generatedSecretName) + }) + client.PrependReactor("create", "secrets", func(action kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, nil + }) + }, + wantActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), + kubetesting.NewCreateAction(secretsGVR, generatedSecretNamespace, generatedSecret), + }, + }, + { + name: "upon updating we discover that the secret has been deleted and our create fails", + storedSecret: func(secret **corev1.Secret) { + (*secret).Data["key"] = []byte("too short") // force updating + }, + apiClient: func(t *testing.T, client *kubernetesfake.Clientset) { + client.PrependReactor("get", "secrets", func(action kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, k8serrors.NewNotFound(secretsGVR.GroupResource(), generatedSecretName) + }) + client.PrependReactor("create", "secrets", func(action kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("some create error") + }) + }, + wantActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), + kubetesting.NewCreateAction(secretsGVR, generatedSecretNamespace, generatedSecret), + }, + wantError: "failed to create/update secret some-namespace/some-name-abc123: failed to create secret: some create error", + }, + { + name: "when generating the secret fails, we return an error", + generateKey: func() ([]byte, error) { + return nil, errors.New("some generate error") + }, + wantError: "failed to generate secret: some generate error", + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + // We cannot currently run this test in parallel since it uses the global generateKey function. + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + + if test.generateKey != nil { + generateKey = test.generateKey + } else { + generateKey = func() ([]byte, error) { + return generatedSymmetricKey, nil + } + } + + apiClient := kubernetesfake.NewSimpleClientset() + if test.apiClient != nil { + test.apiClient(t, apiClient) + } + informerClient := kubernetesfake.NewSimpleClientset() + + storedSecret := generatedSecretWithName.DeepCopy() + if test.storedSecret != nil { + test.storedSecret(&storedSecret) + } + if storedSecret != nil { + require.NoError(t, apiClient.Tracker().Add(storedSecret)) + require.NoError(t, informerClient.Tracker().Add(storedSecret)) + } + + informers := kubeinformers.NewSharedInformerFactory(informerClient, 0) + secrets := informers.Core().V1().Secrets() + + c := New(generatedSecretNamePrefix, apiClient, secrets) + + // Must start informers before calling TestRunSynchronously(). + informers.Start(ctx.Done()) + controllerlib.TestRunSynchronously(t, c) + + err := controllerlib.TestSync(t, c, controllerlib.Context{ + Context: ctx, + Key: controllerlib.Key{ + Namespace: generatedSecretNamespace, + Name: generatedSecretName, + }, + }) + if test.wantError != "" { + require.EqualError(t, err, test.wantError) + } else { + require.NoError(t, err) + } + + if test.wantActions == nil { + test.wantActions = []kubetesting.Action{} + } + require.Equal(t, test.wantActions, apiClient.Actions()) + }) + } +} From 030edaf72da3143afeb1f0672b962522c42566bf Mon Sep 17 00:00:00 2001 From: aram price Date: Thu, 10 Dec 2020 10:51:15 -0800 Subject: [PATCH 03/51] KeyFunc no longer uses multi-value return Signed-off-by: Andrew Keesler --- internal/oidc/dynamiccodec/codec.go | 20 ++++++++++---------- internal/oidc/dynamiccodec/codec_test.go | 18 ++++++++++++------ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/internal/oidc/dynamiccodec/codec.go b/internal/oidc/dynamiccodec/codec.go index 0517e11c..a28c6b08 100644 --- a/internal/oidc/dynamiccodec/codec.go +++ b/internal/oidc/dynamiccodec/codec.go @@ -13,30 +13,30 @@ import ( var _ oidc.Codec = &Codec{} -// KeyFunc returns 2 keys: a required signing key, and an optional encryption key. -type KeyFunc func() ([]byte, []byte) +// KeyFunc returns a single key: a symmetric key. +type KeyFunc func() []byte // Codec can dynamically encode and decode information by using a KeyFunc to get its keys // just-in-time. type Codec struct { - keyFunc KeyFunc + signingKeyFunc KeyFunc + encryptionKeyFunc KeyFunc } -// New creates a new Codec that will use the provided keyFunc for its key source. -func New(keyFunc KeyFunc) *Codec { +// New creates a new Codec that will use the provided keyFuncs for its key source. +func New(signingKeyFunc, encryptionKeyFunc KeyFunc) *Codec { return &Codec{ - keyFunc: keyFunc, + signingKeyFunc: signingKeyFunc, + encryptionKeyFunc: encryptionKeyFunc, } } // Encode implements oidc.Encode(). func (c *Codec) Encode(name string, value interface{}) (string, error) { - signingKey, encryptionKey := c.keyFunc() - return securecookie.New(signingKey, encryptionKey).Encode(name, value) + return securecookie.New(c.signingKeyFunc(), c.encryptionKeyFunc()).Encode(name, value) } // Decode implements oidc.Decode(). func (c *Codec) Decode(name string, value string, into interface{}) error { - signingKey, encryptionKey := c.keyFunc() - return securecookie.New(signingKey, encryptionKey).Decode(name, value, into) + return securecookie.New(c.signingKeyFunc(), c.encryptionKeyFunc()).Decode(name, value, into) } diff --git a/internal/oidc/dynamiccodec/codec_test.go b/internal/oidc/dynamiccodec/codec_test.go index 7513954a..b0db0408 100644 --- a/internal/oidc/dynamiccodec/codec_test.go +++ b/internal/oidc/dynamiccodec/codec_test.go @@ -81,9 +81,12 @@ func TestCodec(t *testing.T) { if test.keys != nil { test.keys(&encoderSigningKey, &encoderEncryptionKey, &decoderSigningKey, &decoderEncryptionKey) } - encoder := New(func() ([]byte, []byte) { - return encoderSigningKey, encoderEncryptionKey - }) + encoder := New(func() []byte { + return encoderSigningKey + }, + func() []byte { + return encoderEncryptionKey + }) encoded, err := encoder.Encode("some-name", "some-message") if test.wantEncoderError != "" { @@ -92,9 +95,12 @@ func TestCodec(t *testing.T) { } require.NoError(t, err) - decoder := New(func() ([]byte, []byte) { - return decoderSigningKey, decoderEncryptionKey - }) + decoder := New(func() []byte { + return decoderSigningKey + }, + func() []byte { + return decoderEncryptionKey + }) var decoded string err = decoder.Decode("some-name", encoded, &decoded) From d8212d1337d211089713efe3106d205186d9c9ea Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Thu, 10 Dec 2020 11:01:03 -0800 Subject: [PATCH 04/51] Whitespace Signed-off-by: aram price --- internal/oidc/dynamiccodec/codec_test.go | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/internal/oidc/dynamiccodec/codec_test.go b/internal/oidc/dynamiccodec/codec_test.go index b0db0408..35a5f8b4 100644 --- a/internal/oidc/dynamiccodec/codec_test.go +++ b/internal/oidc/dynamiccodec/codec_test.go @@ -81,12 +81,8 @@ func TestCodec(t *testing.T) { if test.keys != nil { test.keys(&encoderSigningKey, &encoderEncryptionKey, &decoderSigningKey, &decoderEncryptionKey) } - encoder := New(func() []byte { - return encoderSigningKey - }, - func() []byte { - return encoderEncryptionKey - }) + encoder := New(func() []byte { return encoderSigningKey }, + func() []byte { return encoderEncryptionKey }) encoded, err := encoder.Encode("some-name", "some-message") if test.wantEncoderError != "" { @@ -95,12 +91,8 @@ func TestCodec(t *testing.T) { } require.NoError(t, err) - decoder := New(func() []byte { - return decoderSigningKey - }, - func() []byte { - return decoderEncryptionKey - }) + decoder := New(func() []byte { return decoderSigningKey }, + func() []byte { return decoderEncryptionKey }) var decoded string err = decoder.Decode("some-name", encoded, &decoded) From ccac124b7aa4c7a5076bb30a4a5f0bd3aefe5693 Mon Sep 17 00:00:00 2001 From: aram price Date: Thu, 10 Dec 2020 11:29:13 -0800 Subject: [PATCH 05/51] Fix broken test Signed-off-by: Andrew Keesler --- .../supervisorconfig/secretgenerator/secretgenerator_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/controller/supervisorconfig/secretgenerator/secretgenerator_test.go b/internal/controller/supervisorconfig/secretgenerator/secretgenerator_test.go index f1e5a3de..884e2049 100644 --- a/internal/controller/supervisorconfig/secretgenerator/secretgenerator_test.go +++ b/internal/controller/supervisorconfig/secretgenerator/secretgenerator_test.go @@ -226,6 +226,9 @@ func TestController(t *testing.T) { }, { name: "when generating the secret fails, we return an error", + storedSecret: func(secret **corev1.Secret) { + *secret = nil + }, generateKey: func() ([]byte, error) { return nil, errors.New("some generate error") }, From 1291380611d24c64fed27314c21c779179264ba5 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Thu, 10 Dec 2020 11:34:39 -0800 Subject: [PATCH 06/51] dynamiccodec.Codec uses securecookie.JSONEncoder Signed-off-by: aram price --- internal/oidc/dynamiccodec/codec.go | 12 +++++++++--- internal/oidc/dynamiccodec/codec_test.go | 22 ++++++++++++---------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/internal/oidc/dynamiccodec/codec.go b/internal/oidc/dynamiccodec/codec.go index a28c6b08..13407f70 100644 --- a/internal/oidc/dynamiccodec/codec.go +++ b/internal/oidc/dynamiccodec/codec.go @@ -23,7 +23,9 @@ type Codec struct { encryptionKeyFunc KeyFunc } -// New creates a new Codec that will use the provided keyFuncs for its key source. +// New creates a new Codec that will use the provided keyFuncs for its key source, and +// use the securecookie.JSONEncoder. The securecookie.JSONEncoder is used because the default +// securecookie.GobEncoder is less compact and more difficult to make forward compatible. func New(signingKeyFunc, encryptionKeyFunc KeyFunc) *Codec { return &Codec{ signingKeyFunc: signingKeyFunc, @@ -33,10 +35,14 @@ func New(signingKeyFunc, encryptionKeyFunc KeyFunc) *Codec { // Encode implements oidc.Encode(). func (c *Codec) Encode(name string, value interface{}) (string, error) { - return securecookie.New(c.signingKeyFunc(), c.encryptionKeyFunc()).Encode(name, value) + encoder := securecookie.New(c.signingKeyFunc(), c.encryptionKeyFunc()) + encoder.SetSerializer(securecookie.JSONEncoder{}) + return encoder.Encode(name, value) } // Decode implements oidc.Decode(). func (c *Codec) Decode(name string, value string, into interface{}) error { - return securecookie.New(c.signingKeyFunc(), c.encryptionKeyFunc()).Decode(name, value, into) + decoder := securecookie.New(c.signingKeyFunc(), c.encryptionKeyFunc()) + decoder.SetSerializer(securecookie.JSONEncoder{}) + return decoder.Decode(name, value, into) } diff --git a/internal/oidc/dynamiccodec/codec_test.go b/internal/oidc/dynamiccodec/codec_test.go index 35a5f8b4..e85a77fe 100644 --- a/internal/oidc/dynamiccodec/codec_test.go +++ b/internal/oidc/dynamiccodec/codec_test.go @@ -4,6 +4,7 @@ package dynamiccodec import ( + "strings" "testing" "github.com/stretchr/testify/require" @@ -11,10 +12,10 @@ import ( func TestCodec(t *testing.T) { tests := []struct { - name string - keys func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) - wantEncoderError string - wantDecoderError string + name string + keys func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) + wantEncoderErrorPrefix string + wantDecoderError string }{ { name: "good signing and encryption keys", @@ -31,7 +32,7 @@ func TestCodec(t *testing.T) { keys: func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) { *encoderEncryptionKey = []byte("this-secret-is-not-16-bytes") }, - wantEncoderError: "securecookie: error - caused by: crypto/aes: invalid key size 27", + wantEncoderErrorPrefix: "securecookie: error - caused by: crypto/aes: invalid key size 27", }, { name: "good signing keys and bad decoding encryption key", @@ -45,7 +46,7 @@ func TestCodec(t *testing.T) { keys: func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) { *encoderSigningKey = nil }, - wantEncoderError: "securecookie: hash key is not set", + wantEncoderErrorPrefix: "securecookie: hash key is not set", }, { name: "bad decoder signing key", @@ -66,7 +67,7 @@ func TestCodec(t *testing.T) { keys: func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) { *encoderEncryptionKey = []byte("16-byte-no-match") }, - wantDecoderError: "securecookie: error - caused by: securecookie: error - caused by: gob: encoded unsigned integer out of range", + wantDecoderError: "securecookie: error - caused by: securecookie: error - caused by: invalid character '", }, } for _, test := range tests { @@ -85,8 +86,8 @@ func TestCodec(t *testing.T) { func() []byte { return encoderEncryptionKey }) encoded, err := encoder.Encode("some-name", "some-message") - if test.wantEncoderError != "" { - require.EqualError(t, err, test.wantEncoderError) + if test.wantEncoderErrorPrefix != "" { + require.EqualError(t, err, test.wantEncoderErrorPrefix) return } require.NoError(t, err) @@ -97,7 +98,8 @@ func TestCodec(t *testing.T) { var decoded string err = decoder.Decode("some-name", encoded, &decoded) if test.wantDecoderError != "" { - require.EqualError(t, err, test.wantDecoderError) + require.Error(t, err) + require.True(t, strings.HasPrefix(err.Error(), test.wantDecoderError)) return } require.NoError(t, err) From 2f87be3f94371230ce64d94d3927cbfa007e3640 Mon Sep 17 00:00:00 2001 From: aram price Date: Thu, 10 Dec 2020 11:35:32 -0800 Subject: [PATCH 07/51] Manager uses dynamiccodec.Codec for cookie encoding Signed-off-by: Andrew Keesler --- internal/oidc/provider/manager/manager.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index fc4ff1eb..e6c98d12 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -8,7 +8,8 @@ import ( "strings" "sync" - "github.com/gorilla/securecookie" + "go.pinniped.dev/internal/oidc/dynamiccodec" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "go.pinniped.dev/internal/oidc" @@ -88,14 +89,13 @@ func (m *Manager) SetProviders(oidcProviders ...*provider.OIDCProvider) { // 1. we would like to state to have an embedded expiration date while the cookie does not need that // 2. we would like each downstream provider to use different secrets for signing/encrypting the upstream state, not share secrets // 3. we would like *all* downstream providers to use the *same* signing key for the CSRF cookie (which doesn't need to be encrypted) because cookies are sent per-domain and our issuers can share a domain name (but have different paths) - var upstreamStateEncoderHashKey = []byte("fake-state-hash-secret") // TODO replace this secret - var upstreamStateEncoderBlockKey = []byte("16-bytes-STATE01") // TODO replace this secret - var upstreamStateEncoder = securecookie.New(upstreamStateEncoderHashKey, upstreamStateEncoderBlockKey) - upstreamStateEncoder.SetSerializer(securecookie.JSONEncoder{}) + var upstreamStateEncoderHashKeyFunc = func() []byte { return []byte("fake-state-hash-secret") } // TODO replace this secret + var upstreamStateEncoderBlockKeyFunc = func() []byte { return []byte("16-bytes-STATE01") } // TODO replace this secret + var upstreamStateEncoder = dynamiccodec.New(upstreamStateEncoderHashKeyFunc, upstreamStateEncoderBlockKeyFunc) - var csrfCookieEncoderHashKey = []byte("fake-csrf-hash-secret") // TODO replace this secret - var csrfCookieEncoder = securecookie.New(csrfCookieEncoderHashKey, nil) - csrfCookieEncoder.SetSerializer(securecookie.JSONEncoder{}) + var csrfCookieEncoderHashKeyFunc = func() []byte { return []byte("fake-csrf-hash-secret") } // TODO replace this secret + var csrEncoderBlockKeyFunc = func() []byte { return nil } // TODO replace this secret + var csrfCookieEncoder = dynamiccodec.New(csrfCookieEncoderHashKeyFunc, csrEncoderBlockKeyFunc) m.providerHandlers[(issuerHostWithPath + oidc.WellKnownEndpointPath)] = discovery.NewHandler(issuer) From e067892ffc4c787d75d3a0df570acc66c55b7923 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Thu, 10 Dec 2020 13:54:02 -0800 Subject: [PATCH 08/51] Add secret.Cache to hold crypto inputs --- internal/secret/cache.go | 71 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 internal/secret/cache.go diff --git a/internal/secret/cache.go b/internal/secret/cache.go new file mode 100644 index 00000000..96ccae4a --- /dev/null +++ b/internal/secret/cache.go @@ -0,0 +1,71 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package secret + +type Cache struct { + csrfCookieEncoderHashKey []byte + csrfCookieEncoderBlockKey []byte + oidcProviderCacheMap map[string]*OIDCProviderCache +} + +func (c *Cache) GetCSRFCookieEncoderHashKey() []byte { + return c.csrfCookieEncoderHashKey +} + +func (c *Cache) SetCSRFCookieEncoderHashKey(key []byte) { + c.csrfCookieEncoderHashKey = key +} + +func (c *Cache) GetCSRFCookieEncoderBlockKey() []byte { + return c.csrfCookieEncoderBlockKey +} + +func (c *Cache) SetCSRFCookieEncoderBlockKey(key []byte) { + c.csrfCookieEncoderBlockKey = key +} + +func (c *Cache) GetOIDCProviderCacheFor(oidcIssuer string) *OIDCProviderCache { + return c.oidcProviderCaches()[oidcIssuer] +} + +func (c *Cache) SetOIDCProviderCacheFor(oidcIssuer string, oidcProviderCache *OIDCProviderCache) { + c.oidcProviderCaches()[oidcIssuer] = oidcProviderCache +} + +func (c *Cache) oidcProviderCaches() map[string]*OIDCProviderCache { + if c.oidcProviderCacheMap == nil { + c.oidcProviderCacheMap = map[string]*OIDCProviderCache{} + } + return c.oidcProviderCacheMap +} + +type OIDCProviderCache struct { + tokenHMACKey []byte + stateEncoderHashKey []byte + stateEncoderBlockKey []byte +} + +func (o *OIDCProviderCache) GetTokenHMACKey() []byte { + return o.tokenHMACKey +} + +func (o *OIDCProviderCache) SetTokenHMACKey(key []byte) { + o.tokenHMACKey = key +} + +func (o *OIDCProviderCache) GetStateEncoderHashKey() []byte { + return o.stateEncoderHashKey +} + +func (o *OIDCProviderCache) SetStateEncoderHashKey(key []byte) { + o.stateEncoderHashKey = key +} + +func (o *OIDCProviderCache) GetStateEncoderBlockKey() []byte { + return o.stateEncoderBlockKey +} + +func (o *OIDCProviderCache) SetStateEncoderBlockKey(key []byte) { + o.stateEncoderBlockKey = key +} From 72bc458c8e896004bc7f590a76e5887ba3dfb6a2 Mon Sep 17 00:00:00 2001 From: aram price Date: Thu, 10 Dec 2020 17:18:02 -0800 Subject: [PATCH 09/51] Manager uses secret.Cach with hardcoded values --- internal/oidc/provider/manager/manager.go | 35 +++++++++++++---------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index e6c98d12..973870b3 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -8,6 +8,8 @@ import ( "strings" "sync" + "go.pinniped.dev/internal/secret" + "go.pinniped.dev/internal/oidc/dynamiccodec" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" @@ -72,30 +74,33 @@ func (m *Manager) SetProviders(oidcProviders ...*provider.OIDCProvider) { m.providers = oidcProviders m.providerHandlers = make(map[string]http.Handler) + cache := secret.Cache{} + cache.SetCSRFCookieEncoderHashKey([]byte("fake-csrf-hash-secret")) // TODO fetch from `Secret` + + var csrfCookieEncoder = dynamiccodec.New(cache.GetCSRFCookieEncoderHashKey, cache.GetCSRFCookieEncoderBlockKey) + for _, incomingProvider := range oidcProviders { + providerCache := cache.GetOIDCProviderCacheFor(incomingProvider.Issuer()) + + if providerCache == nil { + providerCache = &secret.OIDCProviderCache{} + providerCache.SetTokenHMACKey([]byte("some secret - must have at least 32 bytes")) // TODO fetch from `Secret` + providerCache.SetStateEncoderHashKey([]byte("fake-state-hash-secret")) // TODO fetch from `Secret` + providerCache.SetStateEncoderBlockKey([]byte("16-bytes-STATE01")) // TODO fetch from `Secret` + cache.SetOIDCProviderCacheFor(incomingProvider.Issuer(), providerCache) + } + issuer := incomingProvider.Issuer() issuerHostWithPath := strings.ToLower(incomingProvider.IssuerHost()) + "/" + incomingProvider.IssuerPath() - fositeHMACSecretForThisProvider := []byte("some secret - must have at least 32 bytes") // TODO replace this secret - // Use NullStorage for the authorize endpoint because we do not actually want to store anything until // the upstream callback endpoint is called later. - oauthHelperWithNullStorage := oidc.FositeOauth2Helper(oidc.NullStorage{}, issuer, fositeHMACSecretForThisProvider, nil, oidc.DefaultOIDCTimeoutsConfiguration()) + oauthHelperWithNullStorage := oidc.FositeOauth2Helper(oidc.NullStorage{}, issuer, providerCache.GetTokenHMACKey(), nil, oidc.DefaultOIDCTimeoutsConfiguration()) // For all the other endpoints, make another oauth helper with exactly the same settings except use real storage. - oauthHelperWithKubeStorage := oidc.FositeOauth2Helper(oidc.NewKubeStorage(m.secretsClient), issuer, fositeHMACSecretForThisProvider, m.dynamicJWKSProvider, oidc.DefaultOIDCTimeoutsConfiguration()) + oauthHelperWithKubeStorage := oidc.FositeOauth2Helper(oidc.NewKubeStorage(m.secretsClient), issuer, providerCache.GetTokenHMACKey(), m.dynamicJWKSProvider, oidc.DefaultOIDCTimeoutsConfiguration()) - // TODO use different codecs for the state and the cookie, because: - // 1. we would like to state to have an embedded expiration date while the cookie does not need that - // 2. we would like each downstream provider to use different secrets for signing/encrypting the upstream state, not share secrets - // 3. we would like *all* downstream providers to use the *same* signing key for the CSRF cookie (which doesn't need to be encrypted) because cookies are sent per-domain and our issuers can share a domain name (but have different paths) - var upstreamStateEncoderHashKeyFunc = func() []byte { return []byte("fake-state-hash-secret") } // TODO replace this secret - var upstreamStateEncoderBlockKeyFunc = func() []byte { return []byte("16-bytes-STATE01") } // TODO replace this secret - var upstreamStateEncoder = dynamiccodec.New(upstreamStateEncoderHashKeyFunc, upstreamStateEncoderBlockKeyFunc) - - var csrfCookieEncoderHashKeyFunc = func() []byte { return []byte("fake-csrf-hash-secret") } // TODO replace this secret - var csrEncoderBlockKeyFunc = func() []byte { return nil } // TODO replace this secret - var csrfCookieEncoder = dynamiccodec.New(csrfCookieEncoderHashKeyFunc, csrEncoderBlockKeyFunc) + var upstreamStateEncoder = dynamiccodec.New(providerCache.GetStateEncoderHashKey, providerCache.GetStateEncoderBlockKey) m.providerHandlers[(issuerHostWithPath + oidc.WellKnownEndpointPath)] = discovery.NewHandler(issuer) From e1173eb5ebc43853f58556414c0b47341f925dd3 Mon Sep 17 00:00:00 2001 From: aram price Date: Thu, 10 Dec 2020 17:27:02 -0800 Subject: [PATCH 10/51] manager.Manager is initialized with secret.Cache - hard-coded secret.Cache is passed in from pinniped-supervisor/main --- cmd/pinniped-supervisor/main.go | 6 ++++++ internal/oidc/provider/manager/manager.go | 14 +++++++------- internal/oidc/provider/manager/manager_test.go | 7 ++++++- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index 31f5dff8..8e61cc38 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -14,6 +14,8 @@ import ( "strings" "time" + "go.pinniped.dev/internal/secret" + "k8s.io/apimachinery/pkg/util/clock" kubeinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" @@ -194,12 +196,16 @@ func run(serverInstallationNamespace string, cfg *supervisor.Config) error { dynamicJWKSProvider := jwks.NewDynamicJWKSProvider() dynamicTLSCertProvider := provider.NewDynamicTLSCertProvider() dynamicUpstreamIDPProvider := provider.NewDynamicUpstreamIDPProvider() + cache := secret.Cache{} + + cache.SetCSRFCookieEncoderHashKey([]byte("fake-csrf-hash-secret")) // TODO fetch from `Secret` // OIDC endpoints will be served by the oidProvidersManager, and any non-OIDC paths will fallback to the healthMux. oidProvidersManager := manager.NewManager( healthMux, dynamicJWKSProvider, dynamicUpstreamIDPProvider, + cache, kubeClient.CoreV1().Secrets(serverInstallationNamespace), ) diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index 973870b3..5846449a 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -37,6 +37,7 @@ type Manager struct { nextHandler http.Handler // the next handler in a chain, called when this manager didn't know how to handle a request dynamicJWKSProvider jwks.DynamicJWKSProvider // in-memory cache of per-issuer JWKS data idpListGetter oidc.IDPListGetter // in-memory cache of upstream IDPs + cache secret.Cache // in-memory cache of cryptographic material secretsClient corev1client.SecretInterface } @@ -48,6 +49,7 @@ func NewManager( nextHandler http.Handler, dynamicJWKSProvider jwks.DynamicJWKSProvider, idpListGetter oidc.IDPListGetter, + cache secret.Cache, secretsClient corev1client.SecretInterface, ) *Manager { return &Manager{ @@ -55,6 +57,7 @@ func NewManager( nextHandler: nextHandler, dynamicJWKSProvider: dynamicJWKSProvider, idpListGetter: idpListGetter, + cache: cache, secretsClient: secretsClient, } } @@ -74,20 +77,17 @@ func (m *Manager) SetProviders(oidcProviders ...*provider.OIDCProvider) { m.providers = oidcProviders m.providerHandlers = make(map[string]http.Handler) - cache := secret.Cache{} - cache.SetCSRFCookieEncoderHashKey([]byte("fake-csrf-hash-secret")) // TODO fetch from `Secret` - - var csrfCookieEncoder = dynamiccodec.New(cache.GetCSRFCookieEncoderHashKey, cache.GetCSRFCookieEncoderBlockKey) + var csrfCookieEncoder = dynamiccodec.New(m.cache.GetCSRFCookieEncoderHashKey, m.cache.GetCSRFCookieEncoderBlockKey) for _, incomingProvider := range oidcProviders { - providerCache := cache.GetOIDCProviderCacheFor(incomingProvider.Issuer()) + providerCache := m.cache.GetOIDCProviderCacheFor(incomingProvider.Issuer()) - if providerCache == nil { + if providerCache == nil { // TODO remove when populated from `Secret` values providerCache = &secret.OIDCProviderCache{} providerCache.SetTokenHMACKey([]byte("some secret - must have at least 32 bytes")) // TODO fetch from `Secret` providerCache.SetStateEncoderHashKey([]byte("fake-state-hash-secret")) // TODO fetch from `Secret` providerCache.SetStateEncoderBlockKey([]byte("16-bytes-STATE01")) // TODO fetch from `Secret` - cache.SetOIDCProviderCacheFor(incomingProvider.Issuer(), providerCache) + m.cache.SetOIDCProviderCacheFor(incomingProvider.Issuer(), providerCache) } issuer := incomingProvider.Issuer() diff --git a/internal/oidc/provider/manager/manager_test.go b/internal/oidc/provider/manager/manager_test.go index 0720b719..a3d6d3d4 100644 --- a/internal/oidc/provider/manager/manager_test.go +++ b/internal/oidc/provider/manager/manager_test.go @@ -14,6 +14,8 @@ import ( "strings" "testing" + "go.pinniped.dev/internal/secret" + "github.com/sclevine/spec" "github.com/stretchr/testify/require" "gopkg.in/square/go-jose.v2" @@ -241,7 +243,10 @@ func TestManager(t *testing.T) { kubeClient = fake.NewSimpleClientset() secretsClient := kubeClient.CoreV1().Secrets("some-namespace") - subject = NewManager(nextHandler, dynamicJWKSProvider, idpListGetter, secretsClient) + cache := secret.Cache{} + cache.SetCSRFCookieEncoderHashKey([]byte("fake-csrf-hash-secret")) + + subject = NewManager(nextHandler, dynamicJWKSProvider, idpListGetter, cache, secretsClient) }) when("given no providers via SetProviders()", func() { From a3285fc1874b3af6521cf8179a3faeb2ce33c4e9 Mon Sep 17 00:00:00 2001 From: aram price Date: Thu, 10 Dec 2020 17:28:47 -0800 Subject: [PATCH 11/51] Fix variable / package name collision --- internal/oidc/provider/manager/manager_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/oidc/provider/manager/manager_test.go b/internal/oidc/provider/manager/manager_test.go index a3d6d3d4..997f5f61 100644 --- a/internal/oidc/provider/manager/manager_test.go +++ b/internal/oidc/provider/manager/manager_test.go @@ -341,7 +341,7 @@ func TestManager(t *testing.T) { r.NoError(err) subject.SetProviders(p1, p2) - jwks := map[string]*jose.JSONWebKeySet{ + jwksMap := map[string]*jose.JSONWebKeySet{ issuer1: {Keys: []jose.JSONWebKey{*newTestJWK(issuer1KeyID)}}, issuer2: {Keys: []jose.JSONWebKey{*newTestJWK(issuer2KeyID)}}, } @@ -349,7 +349,7 @@ func TestManager(t *testing.T) { issuer1: newTestJWK(issuer1KeyID), issuer2: newTestJWK(issuer2KeyID), } - dynamicJWKSProvider.SetIssuerToJWKSMap(jwks, activeJWK) + dynamicJWKSProvider.SetIssuerToJWKSMap(jwksMap, activeJWK) }) it("sends all non-matching host requests to the nextHandler", func() { @@ -384,7 +384,7 @@ func TestManager(t *testing.T) { r.NoError(err) subject.SetProviders(p2, p1) - jwks := map[string]*jose.JSONWebKeySet{ + jwksMap := map[string]*jose.JSONWebKeySet{ issuer1: {Keys: []jose.JSONWebKey{*newTestJWK(issuer1KeyID)}}, issuer2: {Keys: []jose.JSONWebKey{*newTestJWK(issuer2KeyID)}}, } @@ -392,7 +392,7 @@ func TestManager(t *testing.T) { issuer1: newTestJWK(issuer1KeyID), issuer2: newTestJWK(issuer2KeyID), } - dynamicJWKSProvider.SetIssuerToJWKSMap(jwks, activeJWK) + dynamicJWKSProvider.SetIssuerToJWKSMap(jwksMap, activeJWK) }) it("still routes matching requests to the appropriate provider", func() { From 9460b08873e6be712aa6ffabba08e66920eb0d30 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Fri, 11 Dec 2020 11:01:07 -0500 Subject: [PATCH 12/51] Use just-in-time HMAC signing key fetching in our Fosite config This pattern is similar to what we did in 58237d0e7dece5ded997ee9360099b2dd0d508a3. Signed-off-by: Andrew Keesler --- internal/oidc/auth/auth_handler_test.go | 6 +- .../oidc/callback/callback_handler_test.go | 6 +- internal/oidc/dynamic_oauth2_hmac_strategy.go | 98 +++++++++++++++++++ internal/oidc/oidc.go | 4 +- internal/oidc/provider/manager/manager.go | 5 +- internal/oidc/token/token_handler_test.go | 10 +- 6 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 internal/oidc/dynamic_oauth2_hmac_strategy.go diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index d9dc051c..68596000 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -124,10 +124,10 @@ func TestAuthorizationEndpoint(t *testing.T) { // Configure fosite the same way that the production code would, using NullStorage to turn off storage. oauthStore := oidc.NullStorage{} - hmacSecret := []byte("some secret - must have at least 32 bytes") - require.GreaterOrEqual(t, len(hmacSecret), 32, "fosite requires that hmac secrets have at least 32 bytes") + hmacSecretFunc := func() []byte { return []byte("some secret - must have at least 32 bytes") } + require.GreaterOrEqual(t, len(hmacSecretFunc()), 32, "fosite requires that hmac secrets have at least 32 bytes") jwksProviderIsUnused := jwks.NewDynamicJWKSProvider() - oauthHelper := oidc.FositeOauth2Helper(oauthStore, downstreamIssuer, hmacSecret, jwksProviderIsUnused, oidc.DefaultOIDCTimeoutsConfiguration()) + oauthHelper := oidc.FositeOauth2Helper(oauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, oidc.DefaultOIDCTimeoutsConfiguration()) happyCSRF := "test-csrf" happyPKCE := "test-pkce" diff --git a/internal/oidc/callback/callback_handler_test.go b/internal/oidc/callback/callback_handler_test.go index df0c2779..c304af78 100644 --- a/internal/oidc/callback/callback_handler_test.go +++ b/internal/oidc/callback/callback_handler_test.go @@ -458,10 +458,10 @@ func TestCallbackEndpoint(t *testing.T) { // Configure fosite the same way that the production code would. // Inject this into our test subject at the last second so we get a fresh storage for every test. oauthStore := oidc.NewKubeStorage(secrets) - hmacSecret := []byte("some secret - must have at least 32 bytes") - require.GreaterOrEqual(t, len(hmacSecret), 32, "fosite requires that hmac secrets have at least 32 bytes") + hmacSecretFunc := func() []byte { return []byte("some secret - must have at least 32 bytes") } + require.GreaterOrEqual(t, len(hmacSecretFunc()), 32, "fosite requires that hmac secrets have at least 32 bytes") jwksProviderIsUnused := jwks.NewDynamicJWKSProvider() - oauthHelper := oidc.FositeOauth2Helper(oauthStore, downstreamIssuer, hmacSecret, jwksProviderIsUnused, oidc.DefaultOIDCTimeoutsConfiguration()) + oauthHelper := oidc.FositeOauth2Helper(oauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, oidc.DefaultOIDCTimeoutsConfiguration()) idpListGetter := oidctestutil.NewIDPListGetter(&test.idp) subject := NewHandler(idpListGetter, oauthHelper, happyStateCodec, happyCookieCodec, happyUpstreamRedirectURI) diff --git a/internal/oidc/dynamic_oauth2_hmac_strategy.go b/internal/oidc/dynamic_oauth2_hmac_strategy.go new file mode 100644 index 00000000..8d67dc2e --- /dev/null +++ b/internal/oidc/dynamic_oauth2_hmac_strategy.go @@ -0,0 +1,98 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + + "github.com/ory/fosite" + "github.com/ory/fosite/compose" + "github.com/ory/fosite/handler/oauth2" +) + +// dynamicOauth2HMACStrategy is an oauth2.CoreStrategy that can dynamically load an HMAC key to sign +// stuff (access tokens, refresh tokens, and auth codes). We want this dynamic capability since our +// controllers for loading OIDCProvider's and signing keys run in parallel, and thus the signing key +// might not be ready when an OIDCProvider is otherwise ready. +// +// If we ever update OIDCProvider's to hold their signing key, we might not need this type, since we +// could have an invariant that routes to an OIDCProvider's endpoints are only wired up if an +// OIDCProvider has a valid signing key. +type dynamicOauth2HMACStrategy struct { + fositeConfig *compose.Config + keyFunc func() []byte +} + +var _ oauth2.CoreStrategy = &dynamicOauth2HMACStrategy{} + +func newDynamicOauth2HMACStrategy( + fositeConfig *compose.Config, + keyFunc func() []byte, +) *dynamicOauth2HMACStrategy { + return &dynamicOauth2HMACStrategy{ + fositeConfig: fositeConfig, + keyFunc: keyFunc, + } +} + +func (s *dynamicOauth2HMACStrategy) AccessTokenSignature(token string) string { + return s.delegate().AccessTokenSignature(token) +} + +func (s *dynamicOauth2HMACStrategy) GenerateAccessToken( + ctx context.Context, + requester fosite.Requester, +) (token string, signature string, err error) { + return s.delegate().GenerateAccessToken(ctx, requester) +} + +func (s *dynamicOauth2HMACStrategy) ValidateAccessToken( + ctx context.Context, + requester fosite.Requester, + token string, +) (err error) { + return s.delegate().ValidateAccessToken(ctx, requester, token) +} + +func (s *dynamicOauth2HMACStrategy) RefreshTokenSignature(token string) string { + return s.delegate().RefreshTokenSignature(token) +} + +func (s *dynamicOauth2HMACStrategy) GenerateRefreshToken( + ctx context.Context, + requester fosite.Requester, +) (token string, signature string, err error) { + return s.delegate().GenerateRefreshToken(ctx, requester) +} + +func (s *dynamicOauth2HMACStrategy) ValidateRefreshToken( + ctx context.Context, + requester fosite.Requester, + token string, +) (err error) { + return s.delegate().ValidateRefreshToken(ctx, requester, token) +} + +func (s *dynamicOauth2HMACStrategy) AuthorizeCodeSignature(token string) string { + return s.delegate().AuthorizeCodeSignature(token) +} + +func (s *dynamicOauth2HMACStrategy) GenerateAuthorizeCode( + ctx context.Context, + requester fosite.Requester, +) (token string, signature string, err error) { + return s.delegate().GenerateAuthorizeCode(ctx, requester) +} + +func (s *dynamicOauth2HMACStrategy) ValidateAuthorizeCode( + ctx context.Context, + requester fosite.Requester, + token string, +) (err error) { + return s.delegate().ValidateAuthorizeCode(ctx, requester, token) +} + +func (s *dynamicOauth2HMACStrategy) delegate() *oauth2.HMACSHAStrategy { + return compose.NewOAuth2HMACStrategy(s.fositeConfig, s.keyFunc(), nil) +} diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index 76600470..233e93f1 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -187,7 +187,7 @@ func DefaultOIDCTimeoutsConfiguration() TimeoutsConfiguration { func FositeOauth2Helper( oauthStore interface{}, issuer string, - hmacSecretOfLengthAtLeast32 []byte, + hmacSecretOfLengthAtLeast32Func func() []byte, jwksProvider jwks.DynamicJWKSProvider, timeoutsConfiguration TimeoutsConfiguration, ) fosite.OAuth2Provider { @@ -212,7 +212,7 @@ func FositeOauth2Helper( oauthStore, &compose.CommonStrategy{ // Note that Fosite requires the HMAC secret to be at least 32 bytes. - CoreStrategy: compose.NewOAuth2HMACStrategy(oauthConfig, hmacSecretOfLengthAtLeast32, nil), + CoreStrategy: newDynamicOauth2HMACStrategy(oauthConfig, hmacSecretOfLengthAtLeast32Func), OpenIDConnectTokenStrategy: newDynamicOpenIDConnectECDSAStrategy(oauthConfig, jwksProvider), }, nil, // hasher, defaults to using BCrypt when nil. Used for hashing client secrets. diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index 5846449a..b09448df 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -92,13 +92,14 @@ func (m *Manager) SetProviders(oidcProviders ...*provider.OIDCProvider) { issuer := incomingProvider.Issuer() issuerHostWithPath := strings.ToLower(incomingProvider.IssuerHost()) + "/" + incomingProvider.IssuerPath() + oidcTimeouts := oidc.DefaultOIDCTimeoutsConfiguration() // Use NullStorage for the authorize endpoint because we do not actually want to store anything until // the upstream callback endpoint is called later. - oauthHelperWithNullStorage := oidc.FositeOauth2Helper(oidc.NullStorage{}, issuer, providerCache.GetTokenHMACKey(), nil, oidc.DefaultOIDCTimeoutsConfiguration()) + oauthHelperWithNullStorage := oidc.FositeOauth2Helper(oidc.NullStorage{}, issuer, providerCache.GetTokenHMACKey, nil, oidcTimeouts) // For all the other endpoints, make another oauth helper with exactly the same settings except use real storage. - oauthHelperWithKubeStorage := oidc.FositeOauth2Helper(oidc.NewKubeStorage(m.secretsClient), issuer, providerCache.GetTokenHMACKey(), m.dynamicJWKSProvider, oidc.DefaultOIDCTimeoutsConfiguration()) + oauthHelperWithKubeStorage := oidc.FositeOauth2Helper(oidc.NewKubeStorage(m.secretsClient), issuer, providerCache.GetTokenHMACKey, m.dynamicJWKSProvider, oidcTimeouts) var upstreamStateEncoder = dynamiccodec.New(providerCache.GetStateEncoderHashKey, providerCache.GetStateEncoderBlockKey) diff --git a/internal/oidc/token/token_handler_test.go b/internal/oidc/token/token_handler_test.go index d0ae5f3a..37fccf80 100644 --- a/internal/oidc/token/token_handler_test.go +++ b/internal/oidc/token/token_handler_test.go @@ -69,6 +69,10 @@ var ( goodAuthTime = time.Date(1, 2, 3, 4, 5, 6, 7, time.UTC) goodRequestedAtTime = time.Date(7, 6, 5, 4, 3, 2, 1, time.UTC) + hmacSecretFunc = func() []byte { + return []byte(hmacSecret) + } + fositeInvalidMethodErrorBody = func(actual string) string { return here.Docf(` { @@ -1346,7 +1350,7 @@ func makeHappyOauthHelper( t.Helper() jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer) - oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, []byte(hmacSecret), jwkProvider, oidc.DefaultOIDCTimeoutsConfiguration()) + oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, hmacSecretFunc, jwkProvider, oidc.DefaultOIDCTimeoutsConfiguration()) authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper) return oauthHelper, authResponder.GetCode(), jwtSigningKey } @@ -1378,7 +1382,7 @@ func makeOauthHelperWithJWTKeyThatWorksOnlyOnce( t.Helper() jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer) - oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, []byte(hmacSecret), &singleUseJWKProvider{DynamicJWKSProvider: jwkProvider}, oidc.DefaultOIDCTimeoutsConfiguration()) + oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, hmacSecretFunc, &singleUseJWKProvider{DynamicJWKSProvider: jwkProvider}, oidc.DefaultOIDCTimeoutsConfiguration()) authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper) return oauthHelper, authResponder.GetCode(), jwtSigningKey } @@ -1397,7 +1401,7 @@ func makeOauthHelperWithNilPrivateJWTSigningKey( t.Helper() jwkProvider := jwks.NewDynamicJWKSProvider() // empty provider which contains no signing key for this issuer - oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, []byte(hmacSecret), jwkProvider, oidc.DefaultOIDCTimeoutsConfiguration()) + oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, hmacSecretFunc, jwkProvider, oidc.DefaultOIDCTimeoutsConfiguration()) authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper) return oauthHelper, authResponder.GetCode(), nil } From 0246e57d7fd4983724546acfa20d00af4a1f84e9 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Fri, 11 Dec 2020 11:11:10 -0500 Subject: [PATCH 13/51] Set lifespans on state and CSRF cooking encoding Signed-off-by: Andrew Keesler --- internal/oidc/dynamiccodec/codec.go | 24 ++++++++++++++++------- internal/oidc/dynamiccodec/codec_test.go | 23 +++++++++++++++++++--- internal/oidc/oidc.go | 5 +++++ internal/oidc/provider/manager/manager.go | 12 ++++++++++-- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/internal/oidc/dynamiccodec/codec.go b/internal/oidc/dynamiccodec/codec.go index 13407f70..5168b2b7 100644 --- a/internal/oidc/dynamiccodec/codec.go +++ b/internal/oidc/dynamiccodec/codec.go @@ -6,6 +6,8 @@ package dynamiccodec import ( + "time" + "github.com/gorilla/securecookie" "go.pinniped.dev/internal/oidc" @@ -19,6 +21,7 @@ type KeyFunc func() []byte // Codec can dynamically encode and decode information by using a KeyFunc to get its keys // just-in-time. type Codec struct { + lifespan time.Duration signingKeyFunc KeyFunc encryptionKeyFunc KeyFunc } @@ -26,8 +29,12 @@ type Codec struct { // New creates a new Codec that will use the provided keyFuncs for its key source, and // use the securecookie.JSONEncoder. The securecookie.JSONEncoder is used because the default // securecookie.GobEncoder is less compact and more difficult to make forward compatible. -func New(signingKeyFunc, encryptionKeyFunc KeyFunc) *Codec { +// +// The returned Codec will make ensure that the encoded values will only be valid for the provided +// lifespan. +func New(lifespan time.Duration, signingKeyFunc, encryptionKeyFunc KeyFunc) *Codec { return &Codec{ + lifespan: lifespan, signingKeyFunc: signingKeyFunc, encryptionKeyFunc: encryptionKeyFunc, } @@ -35,14 +42,17 @@ func New(signingKeyFunc, encryptionKeyFunc KeyFunc) *Codec { // Encode implements oidc.Encode(). func (c *Codec) Encode(name string, value interface{}) (string, error) { - encoder := securecookie.New(c.signingKeyFunc(), c.encryptionKeyFunc()) - encoder.SetSerializer(securecookie.JSONEncoder{}) - return encoder.Encode(name, value) + return c.delegate().Encode(name, value) } // Decode implements oidc.Decode(). func (c *Codec) Decode(name string, value string, into interface{}) error { - decoder := securecookie.New(c.signingKeyFunc(), c.encryptionKeyFunc()) - decoder.SetSerializer(securecookie.JSONEncoder{}) - return decoder.Decode(name, value, into) + return c.delegate().Decode(name, value, into) +} + +func (c *Codec) delegate() *securecookie.SecureCookie { + codec := securecookie.New(c.signingKeyFunc(), c.encryptionKeyFunc()) + codec.MaxAge(int(c.lifespan.Seconds())) + codec.SetSerializer(securecookie.JSONEncoder{}) + return codec } diff --git a/internal/oidc/dynamiccodec/codec_test.go b/internal/oidc/dynamiccodec/codec_test.go index e85a77fe..e106a55d 100644 --- a/internal/oidc/dynamiccodec/codec_test.go +++ b/internal/oidc/dynamiccodec/codec_test.go @@ -6,6 +6,7 @@ package dynamiccodec import ( "strings" "testing" + "time" "github.com/stretchr/testify/require" ) @@ -13,6 +14,7 @@ import ( func TestCodec(t *testing.T) { tests := []struct { name string + lifespan time.Duration keys func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) wantEncoderErrorPrefix string wantDecoderError string @@ -41,6 +43,11 @@ func TestCodec(t *testing.T) { }, wantDecoderError: "securecookie: error - caused by: crypto/aes: invalid key size 27", }, + { + name: "aaa encoder times stuff out", + lifespan: time.Second, + wantDecoderError: "securecookie: expired timestamp", + }, { name: "bad encoder signing key", keys: func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) { @@ -82,7 +89,13 @@ func TestCodec(t *testing.T) { if test.keys != nil { test.keys(&encoderSigningKey, &encoderEncryptionKey, &decoderSigningKey, &decoderEncryptionKey) } - encoder := New(func() []byte { return encoderSigningKey }, + + lifespan := test.lifespan + if lifespan == 0 { + lifespan = time.Hour + } + + encoder := New(lifespan, func() []byte { return encoderSigningKey }, func() []byte { return encoderEncryptionKey }) encoded, err := encoder.Encode("some-name", "some-message") @@ -92,14 +105,18 @@ func TestCodec(t *testing.T) { } require.NoError(t, err) - decoder := New(func() []byte { return decoderSigningKey }, + if test.lifespan != 0 { + time.Sleep(test.lifespan + time.Second) + } + + decoder := New(lifespan, func() []byte { return decoderSigningKey }, func() []byte { return decoderEncryptionKey }) var decoded string err = decoder.Decode("some-name", encoded, &decoded) if test.wantDecoderError != "" { require.Error(t, err) - require.True(t, strings.HasPrefix(err.Error(), test.wantDecoderError)) + require.True(t, strings.HasPrefix(err.Error(), test.wantDecoderError), "expected %q to start with %q", err.Error(), test.wantDecoderError) return } require.NoError(t, err) diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index 233e93f1..0a798485 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -44,6 +44,11 @@ const ( // CSRFCookieEncodingName is the `name` passed to the encoder for encoding and decoding the CSRF // cookie contents. CSRFCookieEncodingName = "csrf" + + // CSRFCookieLifespan is the length of time that the CSRF cookie is valid. After this time, the + // Supervisor's authorization endpoint should give the browser a new CSRF cookie. We set it to + // a week so that it is unlikely to expire during a login. + CSRFCookieLifespan = time.Hour * 24 * 7 ) // Encoder is the encoding side of the securecookie.Codec interface. diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index b09448df..ad33b408 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -77,7 +77,11 @@ func (m *Manager) SetProviders(oidcProviders ...*provider.OIDCProvider) { m.providers = oidcProviders m.providerHandlers = make(map[string]http.Handler) - var csrfCookieEncoder = dynamiccodec.New(m.cache.GetCSRFCookieEncoderHashKey, m.cache.GetCSRFCookieEncoderBlockKey) + var csrfCookieEncoder = dynamiccodec.New( + oidc.CSRFCookieLifespan, + m.cache.GetCSRFCookieEncoderHashKey, + m.cache.GetCSRFCookieEncoderBlockKey, + ) for _, incomingProvider := range oidcProviders { providerCache := m.cache.GetOIDCProviderCacheFor(incomingProvider.Issuer()) @@ -101,7 +105,11 @@ func (m *Manager) SetProviders(oidcProviders ...*provider.OIDCProvider) { // For all the other endpoints, make another oauth helper with exactly the same settings except use real storage. oauthHelperWithKubeStorage := oidc.FositeOauth2Helper(oidc.NewKubeStorage(m.secretsClient), issuer, providerCache.GetTokenHMACKey, m.dynamicJWKSProvider, oidcTimeouts) - var upstreamStateEncoder = dynamiccodec.New(providerCache.GetStateEncoderHashKey, providerCache.GetStateEncoderBlockKey) + var upstreamStateEncoder = dynamiccodec.New( + oidcTimeouts.UpstreamStateParamLifespan, + providerCache.GetStateEncoderHashKey, + providerCache.GetStateEncoderBlockKey, + ) m.providerHandlers[(issuerHostWithPath + oidc.WellKnownEndpointPath)] = discovery.NewHandler(issuer) From 22c5b102edb61e4cc35e159f9a31533b18bcea84 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Fri, 11 Dec 2020 10:57:20 -0500 Subject: [PATCH 14/51] internal/downward: add support for (optional) pod name Signed-off-by: Andrew Keesler --- internal/downward/downward.go | 12 ++++++++++++ internal/downward/downward_test.go | 9 +++++++++ internal/downward/testdata/valid/name | 1 + internal/downward/testdata/validwithoutname/labels | 2 ++ .../downward/testdata/validwithoutname/namespace | 1 + 5 files changed, 25 insertions(+) create mode 100644 internal/downward/testdata/valid/name create mode 100644 internal/downward/testdata/validwithoutname/labels create mode 100644 internal/downward/testdata/validwithoutname/namespace diff --git a/internal/downward/downward.go b/internal/downward/downward.go index ee4d65b7..75119dc4 100644 --- a/internal/downward/downward.go +++ b/internal/downward/downward.go @@ -13,6 +13,8 @@ import ( "path/filepath" "strconv" "strings" + + "go.pinniped.dev/internal/plog" ) // PodInfo contains pod metadata about the current pod. @@ -20,6 +22,9 @@ type PodInfo struct { // Namespace where the current pod is running. Namespace string + // Name of the current pod. + Name string + // Labels of the current pod. Labels map[string]string } @@ -33,6 +38,13 @@ func Load(directory string) (*PodInfo, error) { } result.Namespace = strings.TrimSpace(string(ns)) + name, err := ioutil.ReadFile(filepath.Join(directory, "name")) + if err != nil { + plog.Warning("could not read 'name' downward API file") + } else { + result.Name = strings.TrimSpace(string(name)) + } + labels, err := ioutil.ReadFile(filepath.Join(directory, "labels")) if err != nil { return nil, fmt.Errorf("could not load labels: %w", err) diff --git a/internal/downward/downward_test.go b/internal/downward/downward_test.go index 9bc954d5..adc15b40 100644 --- a/internal/downward/downward_test.go +++ b/internal/downward/downward_test.go @@ -35,6 +35,15 @@ func TestLoad(t *testing.T) { { name: "valid", inputDir: "./testdata/valid", + want: &PodInfo{ + Namespace: "test-namespace", + Name: "test-name", + Labels: map[string]string{"foo": "bar", "bat": "baz"}, + }, + }, + { + name: "valid without name", + inputDir: "./testdata/validwithoutname", want: &PodInfo{ Namespace: "test-namespace", Labels: map[string]string{"foo": "bar", "bat": "baz"}, diff --git a/internal/downward/testdata/valid/name b/internal/downward/testdata/valid/name new file mode 100644 index 00000000..2fdecf1e --- /dev/null +++ b/internal/downward/testdata/valid/name @@ -0,0 +1 @@ +test-name diff --git a/internal/downward/testdata/validwithoutname/labels b/internal/downward/testdata/validwithoutname/labels new file mode 100644 index 00000000..e5880cda --- /dev/null +++ b/internal/downward/testdata/validwithoutname/labels @@ -0,0 +1,2 @@ +foo="bar" +bat="baz" diff --git a/internal/downward/testdata/validwithoutname/namespace b/internal/downward/testdata/validwithoutname/namespace new file mode 100644 index 00000000..f2605f23 --- /dev/null +++ b/internal/downward/testdata/validwithoutname/namespace @@ -0,0 +1 @@ +test-namespace From e17bc31b29de28d232895863030759d6cf887759 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Fri, 11 Dec 2020 11:11:49 -0500 Subject: [PATCH 15/51] Pass CSRF cookie signing key from controller to cache This also sets the CSRF cookie Secret's OwnerReference to the Pod's grandparent Deployment so that when the Deployment is cleaned up, then the Secret is as well. Obviously this controller implementation has a lot of issues, but it will at least get us started. Signed-off-by: Andrew Keesler --- cmd/pinniped-supervisor/main.go | 71 ++++++++++++-- deploy/supervisor/deployment.yaml | 3 + deploy/supervisor/rbac.yaml | 8 ++ .../secretgenerator/secretgenerator.go | 65 ++++++++----- .../secretgenerator/secretgenerator_test.go | 93 ++++++++++++++----- internal/oidc/provider/manager/manager.go | 4 +- .../oidc/provider/manager/manager_test.go | 2 +- 7 files changed, 190 insertions(+), 56 deletions(-) diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index 8e61cc38..e5b5a6b4 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -16,6 +16,8 @@ import ( "go.pinniped.dev/internal/secret" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/clock" kubeinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" @@ -30,6 +32,7 @@ import ( pinnipedinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/config/supervisor" "go.pinniped.dev/internal/controller/supervisorconfig" + "go.pinniped.dev/internal/controller/supervisorconfig/secretgenerator" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatcher" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/downward" @@ -78,6 +81,8 @@ func startControllers( dynamicJWKSProvider jwks.DynamicJWKSProvider, dynamicTLSCertProvider provider.DynamicTLSCertProvider, dynamicUpstreamIDPProvider provider.DynamicUpstreamIDPProvider, + secretCache *secret.Cache, + supervisorDeployment *appsv1.Deployment, kubeClient kubernetes.Interface, pinnipedClient pinnipedclientset.Interface, kubeInformers kubeinformers.SharedInformerFactory, @@ -126,6 +131,18 @@ func startControllers( ), singletonWorker, ). + WithController( + secretgenerator.New( + supervisorDeployment, + kubeClient, + kubeInformers.Core().V1().Secrets(), + func(secret []byte) { + plog.Debug("setting csrf cookie secret") + secretCache.SetCSRFCookieEncoderHashKey(secret) + }, + ), + singletonWorker, + ). WithController( upstreamwatcher.New( dynamicUpstreamIDPProvider, @@ -145,6 +162,41 @@ func startControllers( go controllerManager.Start(ctx) } +func getSupervisorDeployment( + ctx context.Context, + kubeClient kubernetes.Interface, + podInfo *downward.PodInfo, +) (*appsv1.Deployment, error) { + ns := podInfo.Namespace + + pod, err := kubeClient.CoreV1().Pods(ns).Get(ctx, podInfo.Name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("could not get pod: %w", err) + } + + podOwner := metav1.GetControllerOf(pod) + if podOwner == nil { + return nil, fmt.Errorf("pod %s/%s is missing owner", ns, podInfo.Name) + } + + rs, err := kubeClient.AppsV1().ReplicaSets(ns).Get(ctx, podOwner.Name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("could not get replicaset: %w", err) + } + + rsOwner := metav1.GetControllerOf(rs) + if rsOwner == nil { + return nil, fmt.Errorf("replicaset %s/%s is missing owner", ns, podInfo.Name) + } + + d, err := kubeClient.AppsV1().Deployments(ns).Get(ctx, rsOwner.Name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("could not get deployment: %w", err) + } + + return d, nil +} + func newClients() (kubernetes.Interface, pinnipedclientset.Interface, error) { kubeConfig, err := restclient.InClusterConfig() if err != nil { @@ -166,7 +218,9 @@ func newClients() (kubernetes.Interface, pinnipedclientset.Interface, error) { return kubeClient, pinnipedClient, nil } -func run(serverInstallationNamespace string, cfg *supervisor.Config) error { +func run(podInfo *downward.PodInfo, cfg *supervisor.Config) error { + serverInstallationNamespace := podInfo.Namespace + ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -196,19 +250,22 @@ func run(serverInstallationNamespace string, cfg *supervisor.Config) error { dynamicJWKSProvider := jwks.NewDynamicJWKSProvider() dynamicTLSCertProvider := provider.NewDynamicTLSCertProvider() dynamicUpstreamIDPProvider := provider.NewDynamicUpstreamIDPProvider() - cache := secret.Cache{} - - cache.SetCSRFCookieEncoderHashKey([]byte("fake-csrf-hash-secret")) // TODO fetch from `Secret` + secretCache := secret.Cache{} // OIDC endpoints will be served by the oidProvidersManager, and any non-OIDC paths will fallback to the healthMux. oidProvidersManager := manager.NewManager( healthMux, dynamicJWKSProvider, dynamicUpstreamIDPProvider, - cache, + &secretCache, kubeClient.CoreV1().Secrets(serverInstallationNamespace), ) + supervisorDeployment, err := getSupervisorDeployment(ctx, kubeClient, podInfo) + if err != nil { + return fmt.Errorf("cannot get supervisor deployment: %w", err) + } + startControllers( ctx, cfg, @@ -216,6 +273,8 @@ func run(serverInstallationNamespace string, cfg *supervisor.Config) error { dynamicJWKSProvider, dynamicTLSCertProvider, dynamicUpstreamIDPProvider, + &secretCache, + supervisorDeployment, kubeClient, pinnipedClient, kubeInformers, @@ -284,7 +343,7 @@ func main() { klog.Fatal(fmt.Errorf("could not load config: %w", err)) } - if err := run(podInfo.Namespace, cfg); err != nil { + if err := run(podInfo, cfg); err != nil { klog.Fatal(err) } } diff --git a/deploy/supervisor/deployment.yaml b/deploy/supervisor/deployment.yaml index 28b8d93f..1e0c75c0 100644 --- a/deploy/supervisor/deployment.yaml +++ b/deploy/supervisor/deployment.yaml @@ -132,6 +132,9 @@ spec: - path: "namespace" fieldRef: fieldPath: metadata.namespace + - path: "name" + fieldRef: + fieldPath: metadata.name #! This will help make sure our multiple pods run on different nodes, making #! our deployment "more" "HA". affinity: diff --git a/deploy/supervisor/rbac.yaml b/deploy/supervisor/rbac.yaml index 33e86585..b66c00fc 100644 --- a/deploy/supervisor/rbac.yaml +++ b/deploy/supervisor/rbac.yaml @@ -25,6 +25,14 @@ rules: - apiGroups: [idp.supervisor.pinniped.dev] resources: [upstreamoidcproviders/status] verbs: [get, patch, update] + #! We want to be able to read pods/replicasets/deployment so we can learn who our deployment is to set + #! as an owner reference. + - apiGroups: [""] + resources: [pods] + verbs: [get] + - apiGroups: [apps] + resources: [replicasets,deployments] + verbs: [get] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 diff --git a/internal/controller/supervisorconfig/secretgenerator/secretgenerator.go b/internal/controller/supervisorconfig/secretgenerator/secretgenerator.go index 89df14e1..5a767ce0 100644 --- a/internal/controller/supervisorconfig/secretgenerator/secretgenerator.go +++ b/internal/controller/supervisorconfig/secretgenerator/secretgenerator.go @@ -9,9 +9,11 @@ import ( "crypto/rand" "fmt" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" corev1informers "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/util/retry" @@ -42,22 +44,35 @@ func generateSymmetricKey() ([]byte, error) { } type controller struct { - secretNamePrefix string + owner *appsv1.Deployment client kubernetes.Interface secrets corev1informers.SecretInformer + onCreateOrUpdate func(secret []byte) } // New instantiates a new controllerlib.Controller which will ensure existence of a generated secret. -func New(secretNamePrefix string, client kubernetes.Interface, secrets corev1informers.SecretInformer) controllerlib.Controller { +func New( + owner *appsv1.Deployment, + client kubernetes.Interface, + secrets corev1informers.SecretInformer, + onCreateOrUpdate func(secret []byte), +) controllerlib.Controller { c := controller{ - secretNamePrefix: secretNamePrefix, + owner: owner, client: client, secrets: secrets, + onCreateOrUpdate: onCreateOrUpdate, } - filter := pinnipedcontroller.SimpleFilterWithSingletonQueue(isOwnee) + filter := pinnipedcontroller.SimpleFilter(func(obj metav1.Object) bool { + return metav1.IsControlledBy(obj, owner) + }, nil) return controllerlib.New( - controllerlib.Config{Name: secretNamePrefix + "-secrets-generator", Syncer: &c}, + controllerlib.Config{Name: owner.Name + "-secret-generator", Syncer: &c}, controllerlib.WithInformer(secrets, filter, controllerlib.InformerOption{}), + controllerlib.WithInitialEvent(controllerlib.Key{ + Namespace: owner.Namespace, + Name: owner.Name + "-keys", + }), ) } @@ -72,10 +87,11 @@ func (c *controller) Sync(ctx controllerlib.Context) error { secretNeedsUpdate := isNotFound || !c.isValid(secret) if !secretNeedsUpdate { plog.Debug("secret is up to date", "secret", klog.KObj(secret)) + c.onCreateOrUpdate(secret.Data[symmetricKeySecretDataKey]) return nil } - newSecret, err := c.generateSecret(ctx.Key.Namespace) + newSecret, err := c.generateSecret(ctx.Key.Namespace, ctx.Key.Name) if err != nil { return fmt.Errorf("failed to generate secret: %w", err) } @@ -83,12 +99,14 @@ func (c *controller) Sync(ctx controllerlib.Context) error { if isNotFound { err = c.createSecret(ctx.Context, newSecret) } else { - err = c.updateSecret(ctx.Context, newSecret, ctx.Key.Name) + err = c.updateSecret(ctx.Context, &newSecret, ctx.Key.Name) } if err != nil { - return fmt.Errorf("failed to create/update secret %s/%s: %w", ctx.Key.Namespace, ctx.Key.Name, err) + return fmt.Errorf("failed to create/update secret %s/%s: %w", newSecret.Namespace, newSecret.Name, err) } + c.onCreateOrUpdate(newSecret.Data[symmetricKeySecretDataKey]) + return nil } @@ -108,16 +126,24 @@ func (c *controller) isValid(secret *corev1.Secret) bool { return true } -func (c *controller) generateSecret(namespace string) (*corev1.Secret, error) { +func (c *controller) generateSecret(namespace, name string) (*corev1.Secret, error) { symmetricKey, err := generateKey() if err != nil { return nil, err } + deploymentGVK := schema.GroupVersionKind{ + Group: appsv1.SchemeGroupVersion.Group, + Version: appsv1.SchemeGroupVersion.Version, + Kind: "Deployment", + } return &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - GenerateName: c.secretNamePrefix, - Namespace: namespace, + Name: name, + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(c.owner, deploymentGVK), + }, }, Type: symmetricKeySecretType, Data: map[string][]byte{ @@ -131,8 +157,8 @@ func (c *controller) createSecret(ctx context.Context, newSecret *corev1.Secret) return err } -func (c *controller) updateSecret(ctx context.Context, newSecret *corev1.Secret, secretName string) error { - secrets := c.client.CoreV1().Secrets(newSecret.Namespace) +func (c *controller) updateSecret(ctx context.Context, newSecret **corev1.Secret, secretName string) error { + secrets := c.client.CoreV1().Secrets((*newSecret).Namespace) return retry.RetryOnConflict(retry.DefaultBackoff, func() error { currentSecret, err := secrets.Get(ctx, secretName, metav1.GetOptions{}) isNotFound := k8serrors.IsNotFound(err) @@ -141,26 +167,21 @@ func (c *controller) updateSecret(ctx context.Context, newSecret *corev1.Secret, } if isNotFound { - if err := c.createSecret(ctx, newSecret); err != nil { + if err := c.createSecret(ctx, *newSecret); err != nil { return fmt.Errorf("failed to create secret: %w", err) } return nil } if c.isValid(currentSecret) { + *newSecret = currentSecret return nil } - currentSecret.Type = newSecret.Type - currentSecret.Data = newSecret.Data + currentSecret.Type = (*newSecret).Type + currentSecret.Data = (*newSecret).Data _, err = secrets.Update(ctx, currentSecret, metav1.UpdateOptions{}) return err }) } - -// isOwnee returns whether the provided obj is owned by this controller. -func isOwnee(obj metav1.Object) bool { - // TODO: how do we say we are owned by our Deployment? - return true -} diff --git a/internal/controller/supervisorconfig/secretgenerator/secretgenerator_test.go b/internal/controller/supervisorconfig/secretgenerator/secretgenerator_test.go index 884e2049..f753fcf8 100644 --- a/internal/controller/supervisorconfig/secretgenerator/secretgenerator_test.go +++ b/internal/controller/supervisorconfig/secretgenerator/secretgenerator_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -25,9 +26,9 @@ import ( func TestController(t *testing.T) { const ( - generatedSecretNamespace = "some-namespace" - generatedSecretNamePrefix = "some-name-" - generatedSecretName = "some-name-abc123" + generatedSecretNamespace = "some-namespace" + generatedSecretName = "some-name-abc123" + otherGeneratedSecretName = "some-other-name-abc123" ) var ( @@ -37,32 +38,60 @@ func TestController(t *testing.T) { Resource: "secrets", } - generatedSymmetricKey = []byte("some-neato-32-byte-generated-key") + owner = &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-owner-name", + UID: "some-owner-uid", + }, + } + ownerGVK = schema.GroupVersionKind{ + Group: appsv1.SchemeGroupVersion.Group, + Version: appsv1.SchemeGroupVersion.Version, + Kind: "Deployment", + } + + generatedSymmetricKey = []byte("some-neato-32-byte-generated-key") + otherGeneratedSymmetricKey = []byte("some-funio-32-byte-generated-key") generatedSecret = &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - GenerateName: generatedSecretNamePrefix, - Namespace: generatedSecretNamespace, + Name: generatedSecretName, + Namespace: generatedSecretNamespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(owner, ownerGVK), + }, }, Type: "secrets.pinniped.dev/symmetric", Data: map[string][]byte{ "key": generatedSymmetricKey, }, } - ) - generatedSecretWithName := generatedSecret.DeepCopy() - generatedSecretWithName.Name = generatedSecretName + otherGeneratedSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: generatedSecretName, + Namespace: generatedSecretNamespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(owner, ownerGVK), + }, + }, + Type: "secrets.pinniped.dev/symmetric", + Data: map[string][]byte{ + "key": otherGeneratedSymmetricKey, + }, + } + ) once := sync.Once{} tests := []struct { - name string - storedSecret func(**corev1.Secret) - generateKey func() ([]byte, error) - apiClient func(*testing.T, *kubernetesfake.Clientset) - wantError string - wantActions []kubetesting.Action + name string + storedSecret func(**corev1.Secret) + generateKey func() ([]byte, error) + apiClient func(*testing.T, *kubernetesfake.Clientset) + wantError string + wantActions []kubetesting.Action + wantCallbackSecret []byte }{ { name: "when the secrets does not exist, it gets generated", @@ -72,9 +101,11 @@ func TestController(t *testing.T) { wantActions: []kubetesting.Action{ kubetesting.NewCreateAction(secretsGVR, generatedSecretNamespace, generatedSecret), }, + wantCallbackSecret: generatedSymmetricKey, }, { - name: "when a valid secret exists, nothing happens", + name: "when a valid secret exists, nothing happens", + wantCallbackSecret: generatedSymmetricKey, }, { name: "secret gets updated when the type is wrong", @@ -83,8 +114,9 @@ func TestController(t *testing.T) { }, wantActions: []kubetesting.Action{ kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), - kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName), + kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecret), }, + wantCallbackSecret: generatedSymmetricKey, }, { name: "secret gets updated when the key data does not exist", @@ -93,8 +125,9 @@ func TestController(t *testing.T) { }, wantActions: []kubetesting.Action{ kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), - kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName), + kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecret), }, + wantCallbackSecret: generatedSymmetricKey, }, { name: "secret gets updated when the key data is too short", @@ -103,8 +136,9 @@ func TestController(t *testing.T) { }, wantActions: []kubetesting.Action{ kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), - kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName), + kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecret), }, + wantCallbackSecret: generatedSymmetricKey, }, { name: "an error is returned when creating fails", @@ -133,7 +167,7 @@ func TestController(t *testing.T) { }, wantActions: []kubetesting.Action{ kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), - kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName), + kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecret), }, wantError: "failed to create/update secret some-namespace/some-name-abc123: some update error", }, @@ -168,10 +202,11 @@ func TestController(t *testing.T) { }, wantActions: []kubetesting.Action{ kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), - kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName), + kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecret), kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), - kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName), + kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecret), }, + wantCallbackSecret: generatedSymmetricKey, }, { name: "upon updating we discover that a valid secret exists", @@ -180,12 +215,13 @@ func TestController(t *testing.T) { }, apiClient: func(t *testing.T, client *kubernetesfake.Clientset) { client.PrependReactor("get", "secrets", func(action kubetesting.Action) (bool, runtime.Object, error) { - return true, generatedSecretWithName, nil + return true, otherGeneratedSecret, nil }) }, wantActions: []kubetesting.Action{ kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), }, + wantCallbackSecret: otherGeneratedSymmetricKey, }, { name: "upon updating we discover that the secret has been deleted", @@ -204,6 +240,7 @@ func TestController(t *testing.T) { kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), kubetesting.NewCreateAction(secretsGVR, generatedSecretNamespace, generatedSecret), }, + wantCallbackSecret: generatedSymmetricKey, }, { name: "upon updating we discover that the secret has been deleted and our create fails", @@ -257,7 +294,7 @@ func TestController(t *testing.T) { } informerClient := kubernetesfake.NewSimpleClientset() - storedSecret := generatedSecretWithName.DeepCopy() + storedSecret := generatedSecret.DeepCopy() if test.storedSecret != nil { test.storedSecret(&storedSecret) } @@ -269,7 +306,11 @@ func TestController(t *testing.T) { informers := kubeinformers.NewSharedInformerFactory(informerClient, 0) secrets := informers.Core().V1().Secrets() - c := New(generatedSecretNamePrefix, apiClient, secrets) + var callbackSecret []byte + c := New(owner, apiClient, secrets, func(secret []byte) { + require.Nil(t, callbackSecret, "callback was called twice") + callbackSecret = secret + }) // Must start informers before calling TestRunSynchronously(). informers.Start(ctx.Done()) @@ -292,6 +333,8 @@ func TestController(t *testing.T) { test.wantActions = []kubetesting.Action{} } require.Equal(t, test.wantActions, apiClient.Actions()) + + require.Equal(t, test.wantCallbackSecret, callbackSecret) }) } } diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index ad33b408..6043ba7c 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -37,7 +37,7 @@ type Manager struct { nextHandler http.Handler // the next handler in a chain, called when this manager didn't know how to handle a request dynamicJWKSProvider jwks.DynamicJWKSProvider // in-memory cache of per-issuer JWKS data idpListGetter oidc.IDPListGetter // in-memory cache of upstream IDPs - cache secret.Cache // in-memory cache of cryptographic material + cache *secret.Cache // in-memory cache of cryptographic material secretsClient corev1client.SecretInterface } @@ -49,7 +49,7 @@ func NewManager( nextHandler http.Handler, dynamicJWKSProvider jwks.DynamicJWKSProvider, idpListGetter oidc.IDPListGetter, - cache secret.Cache, + cache *secret.Cache, secretsClient corev1client.SecretInterface, ) *Manager { return &Manager{ diff --git a/internal/oidc/provider/manager/manager_test.go b/internal/oidc/provider/manager/manager_test.go index 997f5f61..0a59abb0 100644 --- a/internal/oidc/provider/manager/manager_test.go +++ b/internal/oidc/provider/manager/manager_test.go @@ -246,7 +246,7 @@ func TestManager(t *testing.T) { cache := secret.Cache{} cache.SetCSRFCookieEncoderHashKey([]byte("fake-csrf-hash-secret")) - subject = NewManager(nextHandler, dynamicJWKSProvider, idpListGetter, cache, secretsClient) + subject = NewManager(nextHandler, dynamicJWKSProvider, idpListGetter, &cache, secretsClient) }) when("given no providers via SetProviders()", func() { From e2aad488524ef54000e1de44431d86c6e77ac4dd Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Fri, 11 Dec 2020 11:46:24 -0500 Subject: [PATCH 16/51] internal/oidc/dynamiccodec: loosen test to reduce flakes When we try to decode with the wrong decryption key, we could get any number of error messages, depending on what failure mode we are in (couldn't authenticate plaintext after decryption, couldn't deserialize, etc.). This change makes the test weaker, but at least we know we will get an error message in the case where the decryption key is wrong. Signed-off-by: Andrew Keesler --- internal/oidc/dynamiccodec/codec_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/oidc/dynamiccodec/codec_test.go b/internal/oidc/dynamiccodec/codec_test.go index e106a55d..bcff2d03 100644 --- a/internal/oidc/dynamiccodec/codec_test.go +++ b/internal/oidc/dynamiccodec/codec_test.go @@ -74,7 +74,7 @@ func TestCodec(t *testing.T) { keys: func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) { *encoderEncryptionKey = []byte("16-byte-no-match") }, - wantDecoderError: "securecookie: error - caused by: securecookie: error - caused by: invalid character '", + wantDecoderError: "securecookie: error - caused by: securecookie: error - caused by: ", }, } for _, test := range tests { From 022dcd1909156612d8d8b75536876a8f1c3cc5c6 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Fri, 11 Dec 2020 15:37:10 -0500 Subject: [PATCH 17/51] Update secretgenerator controller after synchronous review Signed-off-by: Andrew Keesler --- .../secretgenerator/secretgenerator.go | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/internal/controller/supervisorconfig/secretgenerator/secretgenerator.go b/internal/controller/supervisorconfig/secretgenerator/secretgenerator.go index 5a767ce0..a65d9351 100644 --- a/internal/controller/supervisorconfig/secretgenerator/secretgenerator.go +++ b/internal/controller/supervisorconfig/secretgenerator/secretgenerator.go @@ -44,24 +44,27 @@ func generateSymmetricKey() ([]byte, error) { } type controller struct { - owner *appsv1.Deployment - client kubernetes.Interface - secrets corev1informers.SecretInformer - onCreateOrUpdate func(secret []byte) + owner *appsv1.Deployment + client kubernetes.Interface + secrets corev1informers.SecretInformer + setCache func(secret []byte) } // New instantiates a new controllerlib.Controller which will ensure existence of a generated secret. func New( + // TODO: label the generated secret like we do in the JWKSWriterController + // TODO: generate the name for the secret and label the secret with the UID of the owner? So that we don't have naming conflicts if the user has already created a Secret with that name. + // TODO: add tests for the filter like we do in the JWKSWriterController? owner *appsv1.Deployment, client kubernetes.Interface, secrets corev1informers.SecretInformer, - onCreateOrUpdate func(secret []byte), + setCache func(secret []byte), ) controllerlib.Controller { c := controller{ - owner: owner, - client: client, - secrets: secrets, - onCreateOrUpdate: onCreateOrUpdate, + owner: owner, + client: client, + secrets: secrets, + setCache: setCache, } filter := pinnipedcontroller.SimpleFilter(func(obj metav1.Object) bool { return metav1.IsControlledBy(obj, owner) @@ -71,7 +74,7 @@ func New( controllerlib.WithInformer(secrets, filter, controllerlib.InformerOption{}), controllerlib.WithInitialEvent(controllerlib.Key{ Namespace: owner.Namespace, - Name: owner.Name + "-keys", + Name: owner.Name + "-key", }), ) } @@ -87,7 +90,7 @@ func (c *controller) Sync(ctx controllerlib.Context) error { secretNeedsUpdate := isNotFound || !c.isValid(secret) if !secretNeedsUpdate { plog.Debug("secret is up to date", "secret", klog.KObj(secret)) - c.onCreateOrUpdate(secret.Data[symmetricKeySecretDataKey]) + c.setCache(secret.Data[symmetricKeySecretDataKey]) return nil } @@ -105,7 +108,7 @@ func (c *controller) Sync(ctx controllerlib.Context) error { return fmt.Errorf("failed to create/update secret %s/%s: %w", newSecret.Namespace, newSecret.Name, err) } - c.onCreateOrUpdate(newSecret.Data[symmetricKeySecretDataKey]) + c.setCache(newSecret.Data[symmetricKeySecretDataKey]) return nil } From 9e2213cbaeb2eb634ac7141e27a49027445106e3 Mon Sep 17 00:00:00 2001 From: aram price Date: Fri, 11 Dec 2020 16:05:08 -0800 Subject: [PATCH 18/51] Rename for clarity - makes space for OIDCPrivder related controller --- cmd/pinniped-supervisor/main.go | 4 ++-- .../supervisor_secrets.go} | 22 +++++++++---------- .../supervisor_secrets_test.go} | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) rename internal/controller/supervisorconfig/{secretgenerator/secretgenerator.go => generator/supervisor_secrets.go} (85%) rename internal/controller/supervisorconfig/{secretgenerator/secretgenerator_test.go => generator/supervisor_secrets_test.go} (99%) diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index e5b5a6b4..ccc26947 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -32,7 +32,7 @@ import ( pinnipedinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/config/supervisor" "go.pinniped.dev/internal/controller/supervisorconfig" - "go.pinniped.dev/internal/controller/supervisorconfig/secretgenerator" + "go.pinniped.dev/internal/controller/supervisorconfig/generator" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatcher" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/downward" @@ -132,7 +132,7 @@ func startControllers( singletonWorker, ). WithController( - secretgenerator.New( + generator.NewSupervisorSecretsController( supervisorDeployment, kubeClient, kubeInformers.Core().V1().Secrets(), diff --git a/internal/controller/supervisorconfig/secretgenerator/secretgenerator.go b/internal/controller/supervisorconfig/generator/supervisor_secrets.go similarity index 85% rename from internal/controller/supervisorconfig/secretgenerator/secretgenerator.go rename to internal/controller/supervisorconfig/generator/supervisor_secrets.go index a65d9351..7313340d 100644 --- a/internal/controller/supervisorconfig/secretgenerator/secretgenerator.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets.go @@ -1,8 +1,8 @@ // Copyright 2020 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// Package secretgenerator provides a controller that can ensure existence of a generated secret. -package secretgenerator +// Package secretgenerator provides a supervisorSecretsController that can ensure existence of a generated secret. +package generator import ( "context" @@ -43,15 +43,15 @@ func generateSymmetricKey() ([]byte, error) { return b, nil } -type controller struct { +type supervisorSecretsController struct { owner *appsv1.Deployment client kubernetes.Interface secrets corev1informers.SecretInformer setCache func(secret []byte) } -// New instantiates a new controllerlib.Controller which will ensure existence of a generated secret. -func New( +// NewSupervisorSecretsController instantiates a new controllerlib.Controller which will ensure existence of a generated secret. +func NewSupervisorSecretsController( // TODO: label the generated secret like we do in the JWKSWriterController // TODO: generate the name for the secret and label the secret with the UID of the owner? So that we don't have naming conflicts if the user has already created a Secret with that name. // TODO: add tests for the filter like we do in the JWKSWriterController? @@ -60,7 +60,7 @@ func New( secrets corev1informers.SecretInformer, setCache func(secret []byte), ) controllerlib.Controller { - c := controller{ + c := supervisorSecretsController{ owner: owner, client: client, secrets: secrets, @@ -80,7 +80,7 @@ func New( } // Sync implements controllerlib.Syncer.Sync(). -func (c *controller) Sync(ctx controllerlib.Context) error { +func (c *supervisorSecretsController) Sync(ctx controllerlib.Context) error { secret, err := c.secrets.Lister().Secrets(ctx.Key.Namespace).Get(ctx.Key.Name) isNotFound := k8serrors.IsNotFound(err) if !isNotFound && err != nil { @@ -113,7 +113,7 @@ func (c *controller) Sync(ctx controllerlib.Context) error { return nil } -func (c *controller) isValid(secret *corev1.Secret) bool { +func (c *supervisorSecretsController) isValid(secret *corev1.Secret) bool { if secret.Type != symmetricKeySecretType { return false } @@ -129,7 +129,7 @@ func (c *controller) isValid(secret *corev1.Secret) bool { return true } -func (c *controller) generateSecret(namespace, name string) (*corev1.Secret, error) { +func (c *supervisorSecretsController) generateSecret(namespace, name string) (*corev1.Secret, error) { symmetricKey, err := generateKey() if err != nil { return nil, err @@ -155,12 +155,12 @@ func (c *controller) generateSecret(namespace, name string) (*corev1.Secret, err }, nil } -func (c *controller) createSecret(ctx context.Context, newSecret *corev1.Secret) error { +func (c *supervisorSecretsController) createSecret(ctx context.Context, newSecret *corev1.Secret) error { _, err := c.client.CoreV1().Secrets(newSecret.Namespace).Create(ctx, newSecret, metav1.CreateOptions{}) return err } -func (c *controller) updateSecret(ctx context.Context, newSecret **corev1.Secret, secretName string) error { +func (c *supervisorSecretsController) updateSecret(ctx context.Context, newSecret **corev1.Secret, secretName string) error { secrets := c.client.CoreV1().Secrets((*newSecret).Namespace) return retry.RetryOnConflict(retry.DefaultBackoff, func() error { currentSecret, err := secrets.Get(ctx, secretName, metav1.GetOptions{}) diff --git a/internal/controller/supervisorconfig/secretgenerator/secretgenerator_test.go b/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go similarity index 99% rename from internal/controller/supervisorconfig/secretgenerator/secretgenerator_test.go rename to internal/controller/supervisorconfig/generator/supervisor_secrets_test.go index f753fcf8..f782cb5c 100644 --- a/internal/controller/supervisorconfig/secretgenerator/secretgenerator_test.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go @@ -1,7 +1,7 @@ // Copyright 2020 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package secretgenerator +package generator import ( "context" @@ -307,7 +307,7 @@ func TestController(t *testing.T) { secrets := informers.Core().V1().Secrets() var callbackSecret []byte - c := New(owner, apiClient, secrets, func(secret []byte) { + c := NewSupervisorSecretsController(owner, apiClient, secrets, func(secret []byte) { require.Nil(t, callbackSecret, "callback was called twice") callbackSecret = secret }) From 3e31668eb0096f8dbee8a2ba7bdfb6232aec1360 Mon Sep 17 00:00:00 2001 From: aram price Date: Fri, 11 Dec 2020 20:48:45 -0800 Subject: [PATCH 19/51] Refactor some utilitiy methods for sharing. --- .../generator/supervisor_secrets.go | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets.go b/internal/controller/supervisorconfig/generator/supervisor_secrets.go index 7313340d..24d177d7 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets.go @@ -87,14 +87,14 @@ func (c *supervisorSecretsController) Sync(ctx controllerlib.Context) error { return fmt.Errorf("failed to list secret %s/%s: %w", ctx.Key.Namespace, ctx.Key.Name, err) } - secretNeedsUpdate := isNotFound || !c.isValid(secret) + secretNeedsUpdate := isNotFound || !isValid(secret) if !secretNeedsUpdate { plog.Debug("secret is up to date", "secret", klog.KObj(secret)) c.setCache(secret.Data[symmetricKeySecretDataKey]) return nil } - newSecret, err := c.generateSecret(ctx.Key.Namespace, ctx.Key.Name) + newSecret, err := generateSecret(ctx.Key.Namespace, ctx.Key.Name, secretDataFunc, c.owner) if err != nil { return fmt.Errorf("failed to generate secret: %w", err) } @@ -113,7 +113,7 @@ func (c *supervisorSecretsController) Sync(ctx controllerlib.Context) error { return nil } -func (c *supervisorSecretsController) isValid(secret *corev1.Secret) bool { +func isValid(secret *corev1.Secret) bool { if secret.Type != symmetricKeySecretType { return false } @@ -129,12 +129,23 @@ func (c *supervisorSecretsController) isValid(secret *corev1.Secret) bool { return true } -func (c *supervisorSecretsController) generateSecret(namespace, name string) (*corev1.Secret, error) { +func secretDataFunc() (map[string][]byte, error) { symmetricKey, err := generateKey() if err != nil { return nil, err } + return map[string][]byte{ + symmetricKeySecretDataKey: symmetricKey, + }, nil +} + +func generateSecret(namespace, name string, secretDataFunc func() (map[string][]byte, error), owner metav1.Object) (*corev1.Secret, error) { + secretData, err := secretDataFunc() + if err != nil { + return nil, err + } + deploymentGVK := schema.GroupVersionKind{ Group: appsv1.SchemeGroupVersion.Group, Version: appsv1.SchemeGroupVersion.Version, @@ -145,13 +156,11 @@ func (c *supervisorSecretsController) generateSecret(namespace, name string) (*c Name: name, Namespace: namespace, OwnerReferences: []metav1.OwnerReference{ - *metav1.NewControllerRef(c.owner, deploymentGVK), + *metav1.NewControllerRef(owner, deploymentGVK), }, }, Type: symmetricKeySecretType, - Data: map[string][]byte{ - symmetricKeySecretDataKey: symmetricKey, - }, + Data: secretData, }, nil } @@ -176,7 +185,7 @@ func (c *supervisorSecretsController) updateSecret(ctx context.Context, newSecre return nil } - if c.isValid(currentSecret) { + if isValid(currentSecret) { *newSecret = currentSecret return nil } From 3ca877f1df76c094021390aea123f79478ea0eba Mon Sep 17 00:00:00 2001 From: aram price Date: Fri, 11 Dec 2020 20:49:10 -0800 Subject: [PATCH 20/51] WIP - preliminary OIDCProviderSecrets controller Tests not yet passing, controller is incomplete and expectations may be incorrect. --- .../generator/oidc_provider_secrets.go | 209 +++++++ .../generator/oidc_provider_secrets_test.go | 590 ++++++++++++++++++ 2 files changed, 799 insertions(+) create mode 100644 internal/controller/supervisorconfig/generator/oidc_provider_secrets.go create mode 100644 internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go diff --git a/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go new file mode 100644 index 00000000..e1dbab4e --- /dev/null +++ b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go @@ -0,0 +1,209 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package generator + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1informers "k8s.io/client-go/informers/core/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/util/retry" + "k8s.io/klog/v2" + + configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1" + pinnipedclientset "go.pinniped.dev/generated/1.19/client/supervisor/clientset/versioned" + configinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions/config/v1alpha1" + pinnipedcontroller "go.pinniped.dev/internal/controller" + "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/plog" +) + +const ( + // TODO should this live on `provider.OIDCProvider` ? + opcKind = "OIDCProvider" // TODO: deduplicate - internal/controller/supervisorconfig/jwks_writer.go +) + +// jwkController holds the fields necessary for the JWKS controller to communicate with OPC's and +// secrets, both via a cache and via the API. +type oidcProviderSecretsController struct { + secretNameFunc func(*configv1alpha1.OIDCProvider) string + secretLabels map[string]string + secretDataFunc func() (map[string][]byte, error) + pinnipedClient pinnipedclientset.Interface + kubeClient kubernetes.Interface + opcInformer configinformers.OIDCProviderInformer + secretInformer corev1informers.SecretInformer +} + +func NewOIDCProviderSecretsController( + secretNameFunc func(*configv1alpha1.OIDCProvider) string, + secretLabels map[string]string, + secretDataFunc func() (map[string][]byte, error), + kubeClient kubernetes.Interface, + pinnipedClient pinnipedclientset.Interface, + secretInformer corev1informers.SecretInformer, + opcInformer configinformers.OIDCProviderInformer, + withInformer pinnipedcontroller.WithInformerOptionFunc, +) controllerlib.Controller { + return controllerlib.New( + controllerlib.Config{ + Name: "JWKSController", + Syncer: &oidcProviderSecretsController{ + secretNameFunc: secretNameFunc, + secretLabels: secretLabels, + secretDataFunc: secretDataFunc, + kubeClient: kubeClient, + pinnipedClient: pinnipedClient, + secretInformer: secretInformer, + opcInformer: opcInformer, + }, + }, + // We want to be notified when a OPC's secret gets updated or deleted. When this happens, we + // should get notified via the corresponding OPC key. + withInformer( + secretInformer, + controllerlib.FilterFuncs{ + ParentFunc: func(obj metav1.Object) controllerlib.Key { + if isOPCControllee(obj) { + controller := metav1.GetControllerOf(obj) + return controllerlib.Key{ + Name: controller.Name, + Namespace: obj.GetNamespace(), + } + } + return controllerlib.Key{} + }, + AddFunc: isOPCControllee, + UpdateFunc: func(oldObj, newObj metav1.Object) bool { + return isOPCControllee(oldObj) || isOPCControllee(newObj) + }, + DeleteFunc: isOPCControllee, + }, + controllerlib.InformerOption{}, + ), + // We want to be notified when anything happens to an OPC. + withInformer( + opcInformer, + pinnipedcontroller.MatchAnythingFilter(nil), // nil parent func is fine because each event is distinct + controllerlib.InformerOption{}, + ), + ) +} + +func (c *oidcProviderSecretsController) Sync(ctx controllerlib.Context) error { + opc, err := c.opcInformer.Lister().OIDCProviders(ctx.Key.Namespace).Get(ctx.Key.Name) + notFound := k8serrors.IsNotFound(err) + if err != nil && !notFound { + return fmt.Errorf( + "failed to get %s/%s OIDCProvider: %w", + ctx.Key.Namespace, + ctx.Key.Name, + err, + ) + } + + if notFound { + // The corresponding secret to this OPC should have been garbage collected since it should have + // had this OPC as its owner. + plog.Debug( + "oidcprovider deleted", + "oidcprovider", + klog.KRef(ctx.Key.Namespace, ctx.Key.Name), + ) + return nil + } + + secretNeedsUpdate, err := c.secretNeedsUpdate(opc) + if err != nil { + return fmt.Errorf("cannot determine secret status: %w", err) + } + if !secretNeedsUpdate { + // Secret is up to date - we are good to go. + plog.Debug( + "secret is up to date", + "oidcprovider", + klog.KRef(ctx.Key.Namespace, ctx.Key.Name), + ) + return nil + } + + // If the OPC does not have a secret associated with it, that secret does not exist, or the secret + // is invalid, we will generate a new secret (i.e., a JWKS). + secret, err := generateSecret(opc.Namespace, c.secretNameFunc(opc), c.secretDataFunc, opc) + if err != nil { + return fmt.Errorf("cannot generate secret: %w", err) + } + + if err := c.createOrUpdateSecret(ctx.Context, secret); err != nil { + return fmt.Errorf("cannot create or update secret: %w", err) + } + plog.Debug("created/updated secret", "secret", klog.KObj(secret)) + + return nil +} + +func (c *oidcProviderSecretsController) secretNeedsUpdate(opc *configv1alpha1.OIDCProvider) (bool, error) { + // This OPC says it has a secret associated with it. Let's try to get it from the cache. + secret, err := c.secretInformer.Lister().Secrets(opc.Namespace).Get(c.secretNameFunc(opc)) + notFound := k8serrors.IsNotFound(err) + if err != nil && !notFound { + return false, fmt.Errorf("cannot get secret: %w", err) + } + if notFound { + // If we can't find the secret, let's assume we need to create it. + return true, nil + } + + if !isValid(secret) { + // If this secret is invalid, we need to generate a new one. + return true, nil + } + + return false, nil +} + +func (c *oidcProviderSecretsController) createOrUpdateSecret( + ctx context.Context, + newSecret *corev1.Secret, +) error { + secretClient := c.kubeClient.CoreV1().Secrets(newSecret.Namespace) + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + oldSecret, err := secretClient.Get(ctx, newSecret.Name, metav1.GetOptions{}) + notFound := k8serrors.IsNotFound(err) + if err != nil && !notFound { + return fmt.Errorf("cannot get secret: %w", err) + } + + if notFound { + // New secret doesn't exist, so create it. + _, err := secretClient.Create(ctx, newSecret, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("cannot create secret: %w", err) + } + return nil + } + + // New secret already exists, so ensure it is up to date. + if isValid(oldSecret) { + // If the secret already has valid JWK's, then we are good to go and we don't need an update. + return nil + } + + oldSecret.Data = newSecret.Data + _, err = secretClient.Update(ctx, oldSecret, metav1.UpdateOptions{}) + return err + }) +} + +// isOPCControlle returns whether the provided obj is controlled by an OPC. +func isOPCControllee(obj metav1.Object) bool { // TODO: deduplicate - internal/controller/supervisorconfig/jwks_writer.go + controller := metav1.GetControllerOf(obj) + return controller != nil && + controller.APIVersion == configv1alpha1.SchemeGroupVersion.String() && + controller.Kind == opcKind +} diff --git a/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go b/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go new file mode 100644 index 00000000..801b481e --- /dev/null +++ b/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go @@ -0,0 +1,590 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package generator + +import ( + "context" + "errors" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + kubeinformers "k8s.io/client-go/informers" + kubernetesfake "k8s.io/client-go/kubernetes/fake" + kubetesting "k8s.io/client-go/testing" + + configv1alpha1 "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 TestOIDCProviderControllerFilterSecret(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + secret corev1.Secret + wantAdd bool + wantUpdate bool + wantDelete bool + wantParent controllerlib.Key + }{ + { + name: "no owner reference", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{}, + }, + }, + { + name: "owner reference without correct APIVersion", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "OIDCProvider", + Name: "some-name", + Controller: boolPtr(true), + }, + }, + }, + }, + }, + { + name: "owner reference without correct Kind", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: configv1alpha1.SchemeGroupVersion.String(), + Name: "some-name", + Controller: boolPtr(true), + }, + }, + }, + }, + }, + { + name: "owner reference without controller set to true", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: configv1alpha1.SchemeGroupVersion.String(), + Kind: "OIDCProvider", + Name: "some-name", + }, + }, + }, + }, + }, + { + name: "correct owner reference", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: configv1alpha1.SchemeGroupVersion.String(), + Kind: "OIDCProvider", + Name: "some-name", + Controller: boolPtr(true), + }, + }, + }, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + wantParent: controllerlib.Key{Namespace: "some-namespace", Name: "some-name"}, + }, + { + name: "multiple owner references", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "UnrelatedKind", + }, + { + APIVersion: configv1alpha1.SchemeGroupVersion.String(), + Kind: "OIDCProvider", + Name: "some-name", + Controller: boolPtr(true), + }, + }, + }, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + wantParent: controllerlib.Key{Namespace: "some-namespace", Name: "some-name"}, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + secretInformer := kubeinformers.NewSharedInformerFactory( + kubernetesfake.NewSimpleClientset(), + 0, + ).Core().V1().Secrets() + opcInformer := pinnipedinformers.NewSharedInformerFactory( + pinnipedfake.NewSimpleClientset(), + 0, + ).Config().V1alpha1().OIDCProviders() + withInformer := testutil.NewObservableWithInformerOption() + _ = NewOIDCProviderSecretsController( + secretNameFunc, + nil, // labels, not needed + fakeSecretDataFunc, + nil, // kubeClient, not needed + nil, // pinnipedClient, not needed + secretInformer, + opcInformer, + withInformer.WithInformer, + ) + + unrelated := corev1.Secret{} + filter := withInformer.GetFilterForInformer(secretInformer) + require.Equal(t, test.wantAdd, filter.Add(&test.secret)) + require.Equal(t, test.wantUpdate, filter.Update(&unrelated, &test.secret)) + require.Equal(t, test.wantUpdate, filter.Update(&test.secret, &unrelated)) + require.Equal(t, test.wantDelete, filter.Delete(&test.secret)) + require.Equal(t, test.wantParent, filter.Parent(&test.secret)) + }) + } +} + +func TestNewOIDCProviderSecretsControllerFilterOPC(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + opc configv1alpha1.OIDCProvider + wantAdd bool + wantUpdate bool + wantDelete bool + wantParent controllerlib.Key + }{ + { + name: "anything goes", + opc: configv1alpha1.OIDCProvider{}, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + wantParent: controllerlib.Key{}, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + secretInformer := kubeinformers.NewSharedInformerFactory( + kubernetesfake.NewSimpleClientset(), + 0, + ).Core().V1().Secrets() + opcInformer := pinnipedinformers.NewSharedInformerFactory( + pinnipedfake.NewSimpleClientset(), + 0, + ).Config().V1alpha1().OIDCProviders() + withInformer := testutil.NewObservableWithInformerOption() + _ = NewOIDCProviderSecretsController( + secretNameFunc, + nil, // labels, not needed + fakeSecretDataFunc, + nil, // kubeClient, not needed + nil, // pinnipedClient, not needed + secretInformer, + opcInformer, + withInformer.WithInformer, + ) + + unrelated := configv1alpha1.OIDCProvider{} + filter := withInformer.GetFilterForInformer(opcInformer) + require.Equal(t, test.wantAdd, filter.Add(&test.opc)) + require.Equal(t, test.wantUpdate, filter.Update(&unrelated, &test.opc)) + require.Equal(t, test.wantUpdate, filter.Update(&test.opc, &unrelated)) + require.Equal(t, test.wantDelete, filter.Delete(&test.opc)) + require.Equal(t, test.wantParent, filter.Parent(&test.opc)) + }) + } +} + +func TestNewOIDCProviderSecretsControllerSync(t *testing.T) { + // We shouldn't run this test in parallel since it messes with a global function (generateKey). + + const namespace = "tuna-namespace" + + opcGVR := schema.GroupVersionResource{ + Group: configv1alpha1.SchemeGroupVersion.Group, + Version: configv1alpha1.SchemeGroupVersion.Version, + Resource: "oidcproviders", + } + + goodOPC := &configv1alpha1.OIDCProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "good-opc", + Namespace: namespace, + UID: "good-opc-uid", + }, + Spec: configv1alpha1.OIDCProviderSpec{ + Issuer: "https://some-issuer.com", + }, + } + + expectedSecretName := secretNameFunc(goodOPC) + + secretGVR := schema.GroupVersionResource{ + Group: corev1.SchemeGroupVersion.Group, + Version: corev1.SchemeGroupVersion.Version, + Resource: "secrets", + } + + newSecret := func(secretData map[string][]byte) *corev1.Secret { + s := corev1.Secret{ + Type: symmetricKeySecretType, + ObjectMeta: metav1.ObjectMeta{ + Name: expectedSecretName, + Namespace: namespace, + Labels: map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: opcGVR.GroupVersion().String(), + Kind: "OIDCProvider", + Name: goodOPC.Name, + UID: goodOPC.UID, + BlockOwnerDeletion: boolPtr(true), + Controller: boolPtr(true), + }, + }, + }, + Data: secretData, + } + + return &s + } + + secretData, err := fakeSecretDataFunc() + require.NoError(t, err) + + goodSecret := newSecret(secretData) + + tests := []struct { + name string + key controllerlib.Key + secrets []*corev1.Secret + configKubeClient func(*kubernetesfake.Clientset) + configPinnipedClient func(*pinnipedfake.Clientset) + opcs []*configv1alpha1.OIDCProvider + generateKeyErr error + wantGenerateKeyCount int + wantSecretActions []kubetesting.Action + wantOPCActions []kubetesting.Action + wantError string + }{ + { + name: "new opc with no secret", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + wantGenerateKeyCount: 1, + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + kubetesting.NewCreateAction(secretGVR, namespace, goodSecret), + }, + wantOPCActions: []kubetesting.Action{ + kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), + }, + }, + { + name: "opc without status with existing secret", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + secrets: []*corev1.Secret{ + goodSecret, + }, + wantGenerateKeyCount: 1, + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + }, + wantOPCActions: []kubetesting.Action{ + kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), + }, + }, + { + name: "existing opc with no secret", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + wantGenerateKeyCount: 1, + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + kubetesting.NewCreateAction(secretGVR, namespace, goodSecret), + }, + wantOPCActions: []kubetesting.Action{ + kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), + }, + }, + { + name: "existing opc with existing secret", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + secrets: []*corev1.Secret{ + goodSecret, + }, + }, + { + name: "deleted opc", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + // Nothing to do here since Kube will garbage collect our child secret via its OwnerReference. + }, + { + name: "secret data is empty", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + secrets: []*corev1.Secret{ + newSecret(map[string][]byte{}), + }, + wantGenerateKeyCount: 1, + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret), + }, + wantOPCActions: []kubetesting.Action{ + kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), + }, + }, + { + name: fmt.Sprintf("secret missing key %s", symmetricKeySecretDataKey), + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + secrets: []*corev1.Secret{ + newSecret(map[string][]byte{"badKey": []byte("some secret - must have at least 32 bytes")}), + }, + wantGenerateKeyCount: 1, + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret), + }, + wantOPCActions: []kubetesting.Action{ + kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), + }, + }, + { + name: fmt.Sprintf("secret data value for key %s", symmetricKeySecretDataKey), + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + secrets: []*corev1.Secret{ + newSecret(map[string][]byte{symmetricKeySecretDataKey: {}}), + }, + wantGenerateKeyCount: 1, + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret), + }, + wantOPCActions: []kubetesting.Action{ + kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), + }, + }, + { + name: "generate key fails", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + generateKeyErr: errors.New("some generate error"), + wantError: "cannot generate secret: cannot generate key: some generate error", + }, + { + name: "get secret fails", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + configKubeClient: func(client *kubernetesfake.Clientset) { + client.PrependReactor("get", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("some get error") + }) + }, + wantError: "cannot create or update secret: cannot get secret: some get error", + }, + { + name: "create secret fails", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + configKubeClient: func(client *kubernetesfake.Clientset) { + client.PrependReactor("create", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("some create error") + }) + }, + wantError: "cannot create or update secret: cannot create secret: some create error", + }, + { + name: "update secret fails", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + secrets: []*corev1.Secret{ + newSecret(map[string][]byte{}), + }, + configKubeClient: func(client *kubernetesfake.Clientset) { + client.PrependReactor("update", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("some update error") + }) + }, + wantError: "cannot create or update secret: some update error", + }, + { + name: "get opc fails", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + configPinnipedClient: func(client *pinnipedfake.Clientset) { + client.PrependReactor("get", "oidcproviders", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("some get error") + }) + }, + wantError: "cannot update opc: cannot get opc: some get error", + }, + { + name: "update opc fails", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + configPinnipedClient: func(client *pinnipedfake.Clientset) { + client.PrependReactor("update", "oidcproviders", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("some update error") + }) + }, + wantError: "cannot update opc: some update error", + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + // We shouldn't run this test in parallel since it messes with a global function (generateKey). + generateKeyCount := 0 + generateKey := func() (map[string][]byte, error) { + generateKeyCount++ + return map[string][]byte{ + symmetricKeySecretDataKey: []byte("some secret - must have at least 32 bytes"), + }, nil + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + + kubeAPIClient := kubernetesfake.NewSimpleClientset() + kubeInformerClient := kubernetesfake.NewSimpleClientset() + for _, secret := range test.secrets { + require.NoError(t, kubeAPIClient.Tracker().Add(secret)) + require.NoError(t, kubeInformerClient.Tracker().Add(secret)) + } + if test.configKubeClient != nil { + test.configKubeClient(kubeAPIClient) + } + + pinnipedAPIClient := pinnipedfake.NewSimpleClientset() + pinnipedInformerClient := pinnipedfake.NewSimpleClientset() + for _, opc := range test.opcs { + require.NoError(t, pinnipedAPIClient.Tracker().Add(opc)) + require.NoError(t, pinnipedInformerClient.Tracker().Add(opc)) + } + if test.configPinnipedClient != nil { + test.configPinnipedClient(pinnipedAPIClient) + } + + kubeInformers := kubeinformers.NewSharedInformerFactory( + kubeInformerClient, + 0, + ) + pinnipedInformers := pinnipedinformers.NewSharedInformerFactory( + pinnipedInformerClient, + 0, + ) + + c := NewOIDCProviderSecretsController( + secretNameFunc, + map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, + generateKey, + kubeAPIClient, + pinnipedAPIClient, + kubeInformers.Core().V1().Secrets(), + pinnipedInformers.Config().V1alpha1().OIDCProviders(), + controllerlib.WithInformer, + ) + + // Must start informers before calling TestRunSynchronously(). + kubeInformers.Start(ctx.Done()) + pinnipedInformers.Start(ctx.Done()) + controllerlib.TestRunSynchronously(t, c) + + err := controllerlib.TestSync(t, c, controllerlib.Context{ + Context: ctx, + Key: test.key, + }) + if test.wantError != "" { + require.EqualError(t, err, test.wantError) + return + } + require.NoError(t, err) + + require.Equal(t, test.wantGenerateKeyCount, generateKeyCount) + + if test.wantSecretActions != nil { + require.Equal(t, test.wantSecretActions, kubeAPIClient.Actions()) + } + if test.wantOPCActions != nil { + require.Equal(t, test.wantOPCActions, pinnipedAPIClient.Actions()) + } + }) + } +} + +func secretNameFunc(opc *configv1alpha1.OIDCProvider) string { + return fmt.Sprintf("pinniped-%s-%s-test_secret", opc.Kind, opc.UID) +} + +func fakeSecretDataFunc() (map[string][]byte, error) { + return map[string][]byte{ + symmetricKeySecretDataKey: []byte("some secret - must have at least 32 bytes"), + }, nil +} + +func boolPtr(b bool) *bool { return &b } From b043dae1491142ab4227f7c3d8c15ff768ce8dbc Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Mon, 14 Dec 2020 10:36:45 -0500 Subject: [PATCH 21/51] Finish first implementation of generic secret generator controller Signed-off-by: Andrew Keesler --- cmd/pinniped-supervisor/main.go | 80 ++- .../generator/oidc_provider_secrets.go | 113 +++-- .../generator/oidc_provider_secrets_test.go | 473 ++++++++---------- .../generator/supervisor_secrets.go | 1 + .../symmetric_secret_helper.go | 110 ++++ .../symmetric_secret_helper_test.go | 154 ++++++ internal/mocks/mocksecrethelper/generate.go | 6 + .../mocksecrethelper/mocksecrethelper.go | 94 ++++ internal/oidc/provider/manager/manager.go | 8 - .../oidc/provider/manager/manager_test.go | 31 +- internal/secret/cache.go | 10 +- 11 files changed, 753 insertions(+), 327 deletions(-) create mode 100644 internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper.go create mode 100644 internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper_test.go create mode 100644 internal/mocks/mocksecrethelper/generate.go create mode 100644 internal/mocks/mocksecrethelper/mocksecrethelper.go diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index ccc26947..eb2fe2c4 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -5,6 +5,7 @@ package main import ( "context" + "crypto/rand" "crypto/tls" "fmt" "net" @@ -17,6 +18,7 @@ import ( "go.pinniped.dev/internal/secret" appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/clock" kubeinformers "k8s.io/client-go/informers" @@ -28,11 +30,13 @@ import ( "k8s.io/klog/v2" "k8s.io/klog/v2/klogr" + configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1" pinnipedclientset "go.pinniped.dev/generated/1.19/client/supervisor/clientset/versioned" pinnipedinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/config/supervisor" "go.pinniped.dev/internal/controller/supervisorconfig" "go.pinniped.dev/internal/controller/supervisorconfig/generator" + "go.pinniped.dev/internal/controller/supervisorconfig/generator/symmetricsecrethelper" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatcher" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/downward" @@ -88,6 +92,9 @@ func startControllers( kubeInformers kubeinformers.SharedInformerFactory, pinnipedInformers pinnipedinformers.SharedInformerFactory, ) { + opInformer := pinnipedInformers.Config().V1alpha1().OIDCProviders() + secretInformer := kubeInformers.Core().V1().Secrets() + // Create controller manager. controllerManager := controllerlib. NewManager(). @@ -96,7 +103,7 @@ func startControllers( issuerManager, clock.RealClock{}, pinnipedClient, - pinnipedInformers.Config().V1alpha1().OIDCProviders(), + opInformer, controllerlib.WithInformer, ), singletonWorker, @@ -106,8 +113,8 @@ func startControllers( cfg.Labels, kubeClient, pinnipedClient, - kubeInformers.Core().V1().Secrets(), - pinnipedInformers.Config().V1alpha1().OIDCProviders(), + secretInformer, + opInformer, controllerlib.WithInformer, ), singletonWorker, @@ -115,8 +122,8 @@ func startControllers( WithController( supervisorconfig.NewJWKSObserverController( dynamicJWKSProvider, - kubeInformers.Core().V1().Secrets(), - pinnipedInformers.Config().V1alpha1().OIDCProviders(), + secretInformer, + opInformer, controllerlib.WithInformer, ), singletonWorker, @@ -125,8 +132,8 @@ func startControllers( supervisorconfig.NewTLSCertObserverController( dynamicTLSCertProvider, cfg.NamesConfig.DefaultTLSCertificateSecret, - kubeInformers.Core().V1().Secrets(), - pinnipedInformers.Config().V1alpha1().OIDCProviders(), + secretInformer, + opInformer, controllerlib.WithInformer, ), singletonWorker, @@ -135,7 +142,7 @@ func startControllers( generator.NewSupervisorSecretsController( supervisorDeployment, kubeClient, - kubeInformers.Core().V1().Secrets(), + secretInformer, func(secret []byte) { plog.Debug("setting csrf cookie secret") secretCache.SetCSRFCookieEncoderHashKey(secret) @@ -143,6 +150,63 @@ func startControllers( ), singletonWorker, ). + WithController( + generator.NewOIDCProviderSecretsController( + symmetricsecrethelper.New( + "pinniped-oidc-provider-hmac-key-", + cfg.Labels, + rand.Reader, + func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { + plog.Debug("setting hmac secret", "issuer", parent.Spec.Issuer) + secretCache.GetOIDCProviderCacheFor(parent.Spec.Issuer). + SetTokenHMACKey(child.Data[symmetricsecrethelper.SecretDataKey]) + }, + ), + kubeClient, + secretInformer, + opInformer, + controllerlib.WithInformer, + ), + singletonWorker, + ). + WithController( + generator.NewOIDCProviderSecretsController( + symmetricsecrethelper.New( + "pinniped-oidc-provider-upstream-state-signature-key-", + cfg.Labels, + rand.Reader, + func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { + plog.Debug("setting state signature key", "issuer", parent.Spec.Issuer) + secretCache.GetOIDCProviderCacheFor(parent.Spec.Issuer). + SetStateEncoderHashKey(child.Data[symmetricsecrethelper.SecretDataKey]) + }, + ), + kubeClient, + secretInformer, + opInformer, + controllerlib.WithInformer, + ), + singletonWorker, + ). + WithController( + generator.NewOIDCProviderSecretsController( + symmetricsecrethelper.New( + "pinniped-oidc-provider-upstream-state-encryption-key-", + cfg.Labels, + rand.Reader, + func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { + plog.Debug("setting state encryption key", "issuer", parent.Spec.Issuer) + secretCache.GetOIDCProviderCacheFor(parent.Spec.Issuer). + SetStateEncoderHashKey(child.Data[symmetricsecrethelper.SecretDataKey]) + }, + ), + kubeClient, + secretInformer, + opInformer, + controllerlib.WithInformer, + ), + singletonWorker, + ). WithController( upstreamwatcher.New( dynamicUpstreamIDPProvider, diff --git a/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go index e1dbab4e..df3fad84 100644 --- a/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go +++ b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go @@ -16,7 +16,6 @@ import ( "k8s.io/klog/v2" configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1" - pinnipedclientset "go.pinniped.dev/generated/1.19/client/supervisor/clientset/versioned" configinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions/config/v1alpha1" pinnipedcontroller "go.pinniped.dev/internal/controller" "go.pinniped.dev/internal/controllerlib" @@ -28,43 +27,47 @@ const ( opcKind = "OIDCProvider" // TODO: deduplicate - internal/controller/supervisorconfig/jwks_writer.go ) -// jwkController holds the fields necessary for the JWKS controller to communicate with OPC's and -// secrets, both via a cache and via the API. +// SecretHelper describes an object that can Generate() a Secret and determine whether a Secret +// IsValid(). It can also be Notify()'d about a Secret being persisted. +// +// A SecretHelper has a Name() that can be used to identify it from other SecretHelper instances. +type SecretHelper interface { + Name() string + Generate(*configv1alpha1.OIDCProvider) (*corev1.Secret, error) + IsValid(*configv1alpha1.OIDCProvider, *corev1.Secret) bool + Notify(*configv1alpha1.OIDCProvider, *corev1.Secret) +} + type oidcProviderSecretsController struct { - secretNameFunc func(*configv1alpha1.OIDCProvider) string - secretLabels map[string]string - secretDataFunc func() (map[string][]byte, error) - pinnipedClient pinnipedclientset.Interface + secretHelper SecretHelper kubeClient kubernetes.Interface opcInformer configinformers.OIDCProviderInformer secretInformer corev1informers.SecretInformer } +// NewOIDCProviderSecretsController returns a controllerlib.Controller that ensures a child Secret +// always exists for a parent OIDCProvider. It does this using the provided secretHelper, which +// provides the parent/child mapping logic. func NewOIDCProviderSecretsController( - secretNameFunc func(*configv1alpha1.OIDCProvider) string, - secretLabels map[string]string, - secretDataFunc func() (map[string][]byte, error), + secretHelper SecretHelper, kubeClient kubernetes.Interface, - pinnipedClient pinnipedclientset.Interface, secretInformer corev1informers.SecretInformer, opcInformer configinformers.OIDCProviderInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, ) controllerlib.Controller { return controllerlib.New( controllerlib.Config{ - Name: "JWKSController", + Name: fmt.Sprintf("%s%s", secretHelper.Name(), "controller"), Syncer: &oidcProviderSecretsController{ - secretNameFunc: secretNameFunc, - secretLabels: secretLabels, - secretDataFunc: secretDataFunc, + secretHelper: secretHelper, kubeClient: kubeClient, - pinnipedClient: pinnipedClient, secretInformer: secretInformer, opcInformer: opcInformer, }, }, // We want to be notified when a OPC's secret gets updated or deleted. When this happens, we // should get notified via the corresponding OPC key. + // TODO: de-dup me (jwks_writer.go). withInformer( secretInformer, controllerlib.FilterFuncs{ @@ -96,7 +99,7 @@ func NewOIDCProviderSecretsController( } func (c *oidcProviderSecretsController) Sync(ctx controllerlib.Context) error { - opc, err := c.opcInformer.Lister().OIDCProviders(ctx.Key.Namespace).Get(ctx.Key.Name) + op, err := c.opcInformer.Lister().OIDCProviders(ctx.Key.Namespace).Get(ctx.Key.Name) notFound := k8serrors.IsNotFound(err) if err != nil && !notFound { return fmt.Errorf( @@ -108,8 +111,8 @@ func (c *oidcProviderSecretsController) Sync(ctx controllerlib.Context) error { } if notFound { - // The corresponding secret to this OPC should have been garbage collected since it should have - // had this OPC as its owner. + // The corresponding secret to this OP should have been garbage collected since it should have + // had this OP as its owner. plog.Debug( "oidcprovider deleted", "oidcprovider", @@ -118,83 +121,99 @@ func (c *oidcProviderSecretsController) Sync(ctx controllerlib.Context) error { return nil } - secretNeedsUpdate, err := c.secretNeedsUpdate(opc) + newSecret, err := c.secretHelper.Generate(op) if err != nil { - return fmt.Errorf("cannot determine secret status: %w", err) + return fmt.Errorf("failed to generate secret: %w", err) + } + + secretNeedsUpdate, existingSecret, err := c.secretNeedsUpdate(op, newSecret.Name) + if err != nil { + return fmt.Errorf("failed to determine secret status: %w", err) } if !secretNeedsUpdate { // Secret is up to date - we are good to go. plog.Debug( "secret is up to date", "oidcprovider", - klog.KRef(ctx.Key.Namespace, ctx.Key.Name), + klog.KObj(op), + "secret", + klog.KObj(existingSecret), ) + c.secretHelper.Notify(op, existingSecret) return nil } - // If the OPC does not have a secret associated with it, that secret does not exist, or the secret - // is invalid, we will generate a new secret (i.e., a JWKS). - secret, err := generateSecret(opc.Namespace, c.secretNameFunc(opc), c.secretDataFunc, opc) - if err != nil { - return fmt.Errorf("cannot generate secret: %w", err) + // If the OP does not have a secret associated with it, that secret does not exist, or the secret + // is invalid, we will create a new secret. + if err := c.createOrUpdateSecret(ctx.Context, op, &newSecret); err != nil { + return fmt.Errorf("failed to create or update secret: %w", err) } + plog.Debug("created/updated secret", "oidcprovider", klog.KObj(op), "secret", klog.KObj(newSecret)) - if err := c.createOrUpdateSecret(ctx.Context, secret); err != nil { - return fmt.Errorf("cannot create or update secret: %w", err) - } - plog.Debug("created/updated secret", "secret", klog.KObj(secret)) + c.secretHelper.Notify(op, newSecret) return nil } -func (c *oidcProviderSecretsController) secretNeedsUpdate(opc *configv1alpha1.OIDCProvider) (bool, error) { +// secretNeedsUpdate returns whether or not the Secret, with name secretName, for OIDCProvider op +// needs to be updated. It returns the existing secret as its second argument. +func (c *oidcProviderSecretsController) secretNeedsUpdate( + op *configv1alpha1.OIDCProvider, + secretName string, +) (bool, *corev1.Secret, error) { // This OPC says it has a secret associated with it. Let's try to get it from the cache. - secret, err := c.secretInformer.Lister().Secrets(opc.Namespace).Get(c.secretNameFunc(opc)) + secret, err := c.secretInformer.Lister().Secrets(op.Namespace).Get(secretName) notFound := k8serrors.IsNotFound(err) if err != nil && !notFound { - return false, fmt.Errorf("cannot get secret: %w", err) + return false, nil, fmt.Errorf("cannot get secret: %w", err) } if notFound { // If we can't find the secret, let's assume we need to create it. - return true, nil + return true, nil, nil } - if !isValid(secret) { + if !c.secretHelper.IsValid(op, secret) { // If this secret is invalid, we need to generate a new one. - return true, nil + return true, secret, nil } - return false, nil + return false, secret, nil } func (c *oidcProviderSecretsController) createOrUpdateSecret( ctx context.Context, - newSecret *corev1.Secret, + op *configv1alpha1.OIDCProvider, + newSecret **corev1.Secret, ) error { - secretClient := c.kubeClient.CoreV1().Secrets(newSecret.Namespace) + secretClient := c.kubeClient.CoreV1().Secrets((*newSecret).Namespace) return retry.RetryOnConflict(retry.DefaultRetry, func() error { - oldSecret, err := secretClient.Get(ctx, newSecret.Name, metav1.GetOptions{}) + oldSecret, err := secretClient.Get(ctx, (*newSecret).Name, metav1.GetOptions{}) notFound := k8serrors.IsNotFound(err) if err != nil && !notFound { - return fmt.Errorf("cannot get secret: %w", err) + return fmt.Errorf("failed to get secret %s/%s: %w", (*newSecret).Namespace, (*newSecret).Name, err) } if notFound { // New secret doesn't exist, so create it. - _, err := secretClient.Create(ctx, newSecret, metav1.CreateOptions{}) + _, err := secretClient.Create(ctx, *newSecret, metav1.CreateOptions{}) if err != nil { - return fmt.Errorf("cannot create secret: %w", err) + return fmt.Errorf("failed to create secret %s/%s: %w", (*newSecret).Namespace, (*newSecret).Name, err) } return nil } // New secret already exists, so ensure it is up to date. - if isValid(oldSecret) { - // If the secret already has valid JWK's, then we are good to go and we don't need an update. + if c.secretHelper.IsValid(op, oldSecret) { + // If the secret already has valid a valid Secret, then we are good to go and we don't need an + // update. + *newSecret = oldSecret return nil } - oldSecret.Data = newSecret.Data + oldSecret.Labels = (*newSecret).Labels + oldSecret.Type = (*newSecret).Type + oldSecret.Data = (*newSecret).Data + *newSecret = oldSecret _, err = secretClient.Update(ctx, oldSecret, metav1.UpdateOptions{}) return err }) diff --git a/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go b/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go index 801b481e..a271aa22 100644 --- a/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go +++ b/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go @@ -7,11 +7,14 @@ import ( "context" "errors" "fmt" + "sync" "testing" "time" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -23,6 +26,7 @@ import ( 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/mocks/mocksecrethelper" "go.pinniped.dev/internal/testutil" ) @@ -137,6 +141,11 @@ func TestOIDCProviderControllerFilterSecret(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + secretHelper := mocksecrethelper.NewMockSecretHelper(ctrl) + secretHelper.EXPECT().Name().Times(1).Return("some-name") + secretInformer := kubeinformers.NewSharedInformerFactory( kubernetesfake.NewSimpleClientset(), 0, @@ -147,11 +156,8 @@ func TestOIDCProviderControllerFilterSecret(t *testing.T) { ).Config().V1alpha1().OIDCProviders() withInformer := testutil.NewObservableWithInformerOption() _ = NewOIDCProviderSecretsController( - secretNameFunc, - nil, // labels, not needed - fakeSecretDataFunc, + secretHelper, nil, // kubeClient, not needed - nil, // pinnipedClient, not needed secretInformer, opcInformer, withInformer.WithInformer, @@ -193,6 +199,11 @@ func TestNewOIDCProviderSecretsControllerFilterOPC(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + secretHelper := mocksecrethelper.NewMockSecretHelper(ctrl) + secretHelper.EXPECT().Name().Times(1).Return("some-name") + secretInformer := kubeinformers.NewSharedInformerFactory( kubernetesfake.NewSimpleClientset(), 0, @@ -203,11 +214,8 @@ func TestNewOIDCProviderSecretsControllerFilterOPC(t *testing.T) { ).Config().V1alpha1().OIDCProviders() withInformer := testutil.NewObservableWithInformerOption() _ = NewOIDCProviderSecretsController( - secretNameFunc, - nil, // labels, not needed - fakeSecretDataFunc, + secretHelper, nil, // kubeClient, not needed - nil, // pinnipedClient, not needed secretInformer, opcInformer, withInformer.WithInformer, @@ -224,307 +232,271 @@ func TestNewOIDCProviderSecretsControllerFilterOPC(t *testing.T) { } } -func TestNewOIDCProviderSecretsControllerSync(t *testing.T) { - // We shouldn't run this test in parallel since it messes with a global function (generateKey). +func TestOIDCProviderSecretsControllerSync(t *testing.T) { + t.Parallel() - const namespace = "tuna-namespace" + const ( + namespace = "some-namespace" - opcGVR := schema.GroupVersionResource{ + opName = "op-name" + opUID = "op-uid" + + secretName = "secret-name" + secretUID = "secret-uid" + ) + + opGVR := schema.GroupVersionResource{ Group: configv1alpha1.SchemeGroupVersion.Group, Version: configv1alpha1.SchemeGroupVersion.Version, Resource: "oidcproviders", } - goodOPC := &configv1alpha1.OIDCProvider{ - ObjectMeta: metav1.ObjectMeta{ - Name: "good-opc", - Namespace: namespace, - UID: "good-opc-uid", - }, - Spec: configv1alpha1.OIDCProviderSpec{ - Issuer: "https://some-issuer.com", - }, - } - - expectedSecretName := secretNameFunc(goodOPC) - secretGVR := schema.GroupVersionResource{ Group: corev1.SchemeGroupVersion.Group, Version: corev1.SchemeGroupVersion.Version, Resource: "secrets", } - newSecret := func(secretData map[string][]byte) *corev1.Secret { - s := corev1.Secret{ - Type: symmetricKeySecretType, - ObjectMeta: metav1.ObjectMeta{ - Name: expectedSecretName, - Namespace: namespace, - Labels: map[string]string{ - "myLabelKey1": "myLabelValue1", - "myLabelKey2": "myLabelValue2", - }, - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: opcGVR.GroupVersion().String(), - Kind: "OIDCProvider", - Name: goodOPC.Name, - UID: goodOPC.UID, - BlockOwnerDeletion: boolPtr(true), - Controller: boolPtr(true), - }, - }, - }, - Data: secretData, - } - - return &s + goodOP := &configv1alpha1.OIDCProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: opName, + Namespace: namespace, + UID: opUID, + }, } - secretData, err := fakeSecretDataFunc() - require.NoError(t, err) + goodSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + UID: secretUID, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: opGVR.GroupVersion().String(), + Kind: "OIDCProvider", + Name: opName, + UID: opUID, + BlockOwnerDeletion: boolPtr(true), + Controller: boolPtr(true), + }, + }, + Labels: map[string]string{ + "some-key-0": "some-value-0", + "some-key-1": "some-value-1", + }, + }, + Type: "some-secret-type", + Data: map[string][]byte{ + "some-key": []byte("some-value"), + }, + } - goodSecret := newSecret(secretData) + invalidSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + UID: secretUID, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: opGVR.GroupVersion().String(), + Kind: "OIDCProvider", + Name: opName, + UID: opUID, + BlockOwnerDeletion: boolPtr(true), + Controller: boolPtr(true), + }, + }, + }, + } + + once := sync.Once{} tests := []struct { - name string - key controllerlib.Key - secrets []*corev1.Secret - configKubeClient func(*kubernetesfake.Clientset) - configPinnipedClient func(*pinnipedfake.Clientset) - opcs []*configv1alpha1.OIDCProvider - generateKeyErr error - wantGenerateKeyCount int - wantSecretActions []kubetesting.Action - wantOPCActions []kubetesting.Action - wantError string + name string + storage func(**configv1alpha1.OIDCProvider, **corev1.Secret) + client func(*kubernetesfake.Clientset) + secretHelper func(*mocksecrethelper.MockSecretHelper) + wantSecretActions []kubetesting.Action + wantOPActions []kubetesting.Action + wantError string }{ { - name: "new opc with no secret", - key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, - opcs: []*configv1alpha1.OIDCProvider{ - goodOPC, + name: "OIDCProvider exists and secret does not exist", + storage: func(op **configv1alpha1.OIDCProvider, s **corev1.Secret) { + *op = nil + *s = nil + }, + }, + { + name: "OIDCProvider exists and secret exists", + storage: func(op **configv1alpha1.OIDCProvider, s **corev1.Secret) { + *op = nil + }, + }, + { + name: "OIDCProvider exists and secret does not exist", + storage: func(op **configv1alpha1.OIDCProvider, s **corev1.Secret) { + *s = nil + }, + secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { + secretHelper.EXPECT().Generate(goodOP).Times(1).Return(goodSecret, nil) + secretHelper.EXPECT().Notify(goodOP, goodSecret).Times(1) }, - wantGenerateKeyCount: 1, wantSecretActions: []kubetesting.Action{ kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), kubetesting.NewCreateAction(secretGVR, namespace, goodSecret), }, - wantOPCActions: []kubetesting.Action{ - kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), + }, + { + name: "OIDCProvider exists and valid secret exists", + secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { + secretHelper.EXPECT().Generate(goodOP).Times(1).Return(goodSecret, nil) + secretHelper.EXPECT().IsValid(goodOP, goodSecret).Times(1).Return(true) + secretHelper.EXPECT().Notify(goodOP, goodSecret).Times(1) }, }, { - name: "opc without status with existing secret", - key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, - opcs: []*configv1alpha1.OIDCProvider{ - goodOPC, + name: "OIDCProvider exists and invalid secret exists", + storage: func(op **configv1alpha1.OIDCProvider, s **corev1.Secret) { + *s = invalidSecret.DeepCopy() }, - secrets: []*corev1.Secret{ - goodSecret, + secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { + secretHelper.EXPECT().Generate(goodOP).Times(1).Return(goodSecret, nil) + secretHelper.EXPECT().IsValid(goodOP, invalidSecret).Times(2).Return(false) + secretHelper.EXPECT().Notify(goodOP, goodSecret).Times(1) }, - wantGenerateKeyCount: 1, - wantSecretActions: []kubetesting.Action{ - kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), - }, - wantOPCActions: []kubetesting.Action{ - kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), - }, - }, - { - name: "existing opc with no secret", - key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, - opcs: []*configv1alpha1.OIDCProvider{ - goodOPC, - }, - wantGenerateKeyCount: 1, - wantSecretActions: []kubetesting.Action{ - kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), - kubetesting.NewCreateAction(secretGVR, namespace, goodSecret), - }, - wantOPCActions: []kubetesting.Action{ - kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), - }, - }, - { - name: "existing opc with existing secret", - key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, - opcs: []*configv1alpha1.OIDCProvider{ - goodOPC, - }, - secrets: []*corev1.Secret{ - goodSecret, - }, - }, - { - name: "deleted opc", - key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, - // Nothing to do here since Kube will garbage collect our child secret via its OwnerReference. - }, - { - name: "secret data is empty", - key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, - opcs: []*configv1alpha1.OIDCProvider{ - goodOPC, - }, - secrets: []*corev1.Secret{ - newSecret(map[string][]byte{}), - }, - wantGenerateKeyCount: 1, wantSecretActions: []kubetesting.Action{ kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret), }, - wantOPCActions: []kubetesting.Action{ - kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), - }, }, { - name: fmt.Sprintf("secret missing key %s", symmetricKeySecretDataKey), - key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, - opcs: []*configv1alpha1.OIDCProvider{ - goodOPC, + name: "OIDCProvider exists and generating a secret fails", + secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { + secretHelper.EXPECT().Generate(goodOP).Times(1).Return(nil, errors.New("some generate error")) }, - secrets: []*corev1.Secret{ - newSecret(map[string][]byte{"badKey": []byte("some secret - must have at least 32 bytes")}), + wantError: "failed to generate secret: some generate error", + }, + { + name: "OIDCProvider exists and invalid secret exists and upon update we learn of a valid secret", + secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { + otherSecret := goodSecret.DeepCopy() + otherSecret.UID = "other-secret-uid" + + secretHelper.EXPECT().Generate(goodOP).Times(1).Return(otherSecret, nil) + secretHelper.EXPECT().IsValid(goodOP, goodSecret).Times(1).Return(false) + secretHelper.EXPECT().IsValid(goodOP, goodSecret).Times(1).Return(true) + secretHelper.EXPECT().Notify(goodOP, goodSecret).Times(1) }, - wantGenerateKeyCount: 1, wantSecretActions: []kubetesting.Action{ kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), - kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret), - }, - wantOPCActions: []kubetesting.Action{ - kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), }, }, { - name: fmt.Sprintf("secret data value for key %s", symmetricKeySecretDataKey), - key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, - opcs: []*configv1alpha1.OIDCProvider{ - goodOPC, + name: "OIDCProvider exists and invalid secret exists and get fails", + secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { + secretHelper.EXPECT().Generate(goodOP).Times(1).Return(goodSecret, nil) + secretHelper.EXPECT().IsValid(goodOP, goodSecret).Times(1).Return(false) }, - secrets: []*corev1.Secret{ - newSecret(map[string][]byte{symmetricKeySecretDataKey: {}}), - }, - wantGenerateKeyCount: 1, - wantSecretActions: []kubetesting.Action{ - kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), - kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret), - }, - wantOPCActions: []kubetesting.Action{ - kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), - }, - }, - { - name: "generate key fails", - key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, - opcs: []*configv1alpha1.OIDCProvider{ - goodOPC, - }, - generateKeyErr: errors.New("some generate error"), - wantError: "cannot generate secret: cannot generate key: some generate error", - }, - { - name: "get secret fails", - key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, - opcs: []*configv1alpha1.OIDCProvider{ - goodOPC, - }, - configKubeClient: func(client *kubernetesfake.Clientset) { - client.PrependReactor("get", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { + client: func(c *kubernetesfake.Clientset) { + c.PrependReactor("get", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { return true, nil, errors.New("some get error") }) }, - wantError: "cannot create or update secret: cannot get secret: some get error", + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + }, + wantError: fmt.Sprintf("failed to create or update secret: failed to get secret %s/%s: some get error", namespace, goodSecret.Name), }, { - name: "create secret fails", - key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, - opcs: []*configv1alpha1.OIDCProvider{ - goodOPC, + name: "OIDCProvider exists and secret does not exist and create fails", + storage: func(op **configv1alpha1.OIDCProvider, s **corev1.Secret) { + *s = nil }, - configKubeClient: func(client *kubernetesfake.Clientset) { - client.PrependReactor("create", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { + secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { + secretHelper.EXPECT().Generate(goodOP).Times(1).Return(goodSecret, nil) + }, + client: func(c *kubernetesfake.Clientset) { + c.PrependReactor("create", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { return true, nil, errors.New("some create error") }) }, - wantError: "cannot create or update secret: cannot create secret: some create error", + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + kubetesting.NewCreateAction(secretGVR, namespace, goodSecret), + }, + wantError: fmt.Sprintf("failed to create or update secret: failed to create secret %s/%s: some create error", namespace, goodSecret.Name), }, { - name: "update secret fails", - key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, - opcs: []*configv1alpha1.OIDCProvider{ - goodOPC, + name: "OIDCProvider exists and invalid secret exists and update fails", + secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { + secretHelper.EXPECT().Generate(goodOP).Times(1).Return(goodSecret, nil) + secretHelper.EXPECT().IsValid(goodOP, goodSecret).Times(2).Return(false) }, - secrets: []*corev1.Secret{ - newSecret(map[string][]byte{}), - }, - configKubeClient: func(client *kubernetesfake.Clientset) { - client.PrependReactor("update", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { + client: func(c *kubernetesfake.Clientset) { + c.PrependReactor("update", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { return true, nil, errors.New("some update error") }) }, - wantError: "cannot create or update secret: some update error", + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret), + }, + wantError: "failed to create or update secret: some update error", }, { - name: "get opc fails", - key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, - opcs: []*configv1alpha1.OIDCProvider{ - goodOPC, + name: "OIDCProvider exists and invalid secret exists and update fails due to conflict", + storage: func(op **configv1alpha1.OIDCProvider, s **corev1.Secret) { + *s = invalidSecret.DeepCopy() }, - configPinnipedClient: func(client *pinnipedfake.Clientset) { - client.PrependReactor("get", "oidcproviders", func(_ kubetesting.Action) (bool, runtime.Object, error) { - return true, nil, errors.New("some get error") + secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { + secretHelper.EXPECT().Generate(goodOP).Times(1).Return(goodSecret, nil) + secretHelper.EXPECT().IsValid(goodOP, invalidSecret).Times(3).Return(false) + secretHelper.EXPECT().Notify(goodOP, goodSecret).Times(1) + }, + client: func(c *kubernetesfake.Clientset) { + c.PrependReactor("update", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { + var err error + once.Do(func() { err = k8serrors.NewConflict(secretGVR.GroupResource(), namespace, errors.New("some error")) }) + return true, nil, err }) }, - wantError: "cannot update opc: cannot get opc: some get error", - }, - { - name: "update opc fails", - key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, - opcs: []*configv1alpha1.OIDCProvider{ - goodOPC, + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret), + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret), }, - configPinnipedClient: func(client *pinnipedfake.Clientset) { - client.PrependReactor("update", "oidcproviders", func(_ kubetesting.Action) (bool, runtime.Object, error) { - return true, nil, errors.New("some update error") - }) - }, - wantError: "cannot update opc: some update error", }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - // We shouldn't run this test in parallel since it messes with a global function (generateKey). - generateKeyCount := 0 - generateKey := func() (map[string][]byte, error) { - generateKeyCount++ - return map[string][]byte{ - symmetricKeySecretDataKey: []byte("some secret - must have at least 32 bytes"), - }, nil - } + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) defer cancel() + pinnipedInformerClient := pinnipedfake.NewSimpleClientset() + kubeAPIClient := kubernetesfake.NewSimpleClientset() kubeInformerClient := kubernetesfake.NewSimpleClientset() - for _, secret := range test.secrets { + + op := goodOP.DeepCopy() + secret := goodSecret.DeepCopy() + if test.storage != nil { + test.storage(&op, &secret) + } + if op != nil { + require.NoError(t, pinnipedInformerClient.Tracker().Add(op)) + } + if secret != nil { require.NoError(t, kubeAPIClient.Tracker().Add(secret)) require.NoError(t, kubeInformerClient.Tracker().Add(secret)) } - if test.configKubeClient != nil { - test.configKubeClient(kubeAPIClient) - } - pinnipedAPIClient := pinnipedfake.NewSimpleClientset() - pinnipedInformerClient := pinnipedfake.NewSimpleClientset() - for _, opc := range test.opcs { - require.NoError(t, pinnipedAPIClient.Tracker().Add(opc)) - require.NoError(t, pinnipedInformerClient.Tracker().Add(opc)) - } - if test.configPinnipedClient != nil { - test.configPinnipedClient(pinnipedAPIClient) + if test.client != nil { + test.client(kubeAPIClient) } kubeInformers := kubeinformers.NewSharedInformerFactory( @@ -536,15 +508,17 @@ func TestNewOIDCProviderSecretsControllerSync(t *testing.T) { 0, ) + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + secretHelper := mocksecrethelper.NewMockSecretHelper(ctrl) + secretHelper.EXPECT().Name().Times(1).Return("some-name") + if test.secretHelper != nil { + test.secretHelper(secretHelper) + } + c := NewOIDCProviderSecretsController( - secretNameFunc, - map[string]string{ - "myLabelKey1": "myLabelValue1", - "myLabelKey2": "myLabelValue2", - }, - generateKey, + secretHelper, kubeAPIClient, - pinnipedAPIClient, kubeInformers.Core().V1().Secrets(), pinnipedInformers.Config().V1alpha1().OIDCProviders(), controllerlib.WithInformer, @@ -557,7 +531,10 @@ func TestNewOIDCProviderSecretsControllerSync(t *testing.T) { err := controllerlib.TestSync(t, c, controllerlib.Context{ Context: ctx, - Key: test.key, + Key: controllerlib.Key{ + Namespace: namespace, + Name: opName, + }, }) if test.wantError != "" { require.EqualError(t, err, test.wantError) @@ -565,26 +542,12 @@ func TestNewOIDCProviderSecretsControllerSync(t *testing.T) { } require.NoError(t, err) - require.Equal(t, test.wantGenerateKeyCount, generateKeyCount) - - if test.wantSecretActions != nil { - require.Equal(t, test.wantSecretActions, kubeAPIClient.Actions()) - } - if test.wantOPCActions != nil { - require.Equal(t, test.wantOPCActions, pinnipedAPIClient.Actions()) + if test.wantSecretActions == nil { + test.wantSecretActions = []kubetesting.Action{} } + require.Equal(t, test.wantSecretActions, kubeAPIClient.Actions()) }) } } -func secretNameFunc(opc *configv1alpha1.OIDCProvider) string { - return fmt.Sprintf("pinniped-%s-%s-test_secret", opc.Kind, opc.UID) -} - -func fakeSecretDataFunc() (map[string][]byte, error) { - return map[string][]byte{ - symmetricKeySecretDataKey: []byte("some secret - must have at least 32 bytes"), - }, nil -} - func boolPtr(b bool) *bool { return &b } diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets.go b/internal/controller/supervisorconfig/generator/supervisor_secrets.go index 24d177d7..cc3a9d33 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets.go @@ -24,6 +24,7 @@ import ( "go.pinniped.dev/internal/plog" ) +// TODO: de-dup me when we abstract these controllers. const ( symmetricKeySecretType = "secrets.pinniped.dev/symmetric" symmetricKeySecretDataKey = "key" diff --git a/internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper.go b/internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper.go new file mode 100644 index 00000000..f7baecf3 --- /dev/null +++ b/internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper.go @@ -0,0 +1,110 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package symmetricsecrethelper provides a type that can generate and validate symmetric keys as +// Secret's. +package symmetricsecrethelper + +import ( + "fmt" + "io" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1" + "go.pinniped.dev/internal/controller/supervisorconfig/generator" +) + +const ( + // SecretType is corev1.Secret.Type of all corev1.Secret's generated by this helper. + SecretType = "secrets.pinniped.dev/symmetric" + // SecretDataKey is the corev1.Secret.Data key for the symmetric key value generated by this helper. + SecretDataKey = "key" + + // keySize is the default length, in bytes, of generated keys. It is set to 32 since this + // seems like reasonable entropy for our keys, and a 32-byte key will allow for AES-256 + // to be used in our codecs (see dynamiccodec.Codec). + keySize = 32 +) + +type helper struct { + namePrefix string + labels map[string]string + rand io.Reader + notifyFunc func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) +} + +var _ generator.SecretHelper = &helper{} + +// New returns a SecretHelper that has been parameterized with common symmetric secret generation +// knobs. +func New( + namePrefix string, + labels map[string]string, + rand io.Reader, + notifyFunc func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret), +) generator.SecretHelper { + return &helper{ + namePrefix: namePrefix, + labels: labels, + rand: rand, + notifyFunc: notifyFunc, + } +} + +func (s *helper) Name() string { return s.namePrefix } + +// Generate implements SecretHelper.Generate(). +func (s *helper) Generate(parent *configv1alpha1.OIDCProvider) (*corev1.Secret, error) { + key := make([]byte, keySize) + if _, err := s.rand.Read(key); err != nil { + return nil, err + } + + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s%s", s.namePrefix, parent.UID), + Namespace: parent.Namespace, + Labels: s.labels, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(parent, schema.GroupVersionKind{ + Group: configv1alpha1.SchemeGroupVersion.Group, + Version: configv1alpha1.SchemeGroupVersion.Version, + Kind: "OIDCProvider", + }), + }, + }, + Type: SecretType, + Data: map[string][]byte{ + SecretDataKey: key, + }, + }, nil +} + +// IsValid implements SecretHelper.IsValid(). +func (s *helper) IsValid(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) bool { + if !metav1.IsControlledBy(child, parent) { + return false + } + + if child.Type != SecretType { + return false + } + + key, ok := child.Data[SecretDataKey] + if !ok { + return false + } + if len(key) != keySize { + return false + } + + return true +} + +// Notify implements SecretHelper.Notify(). +func (s *helper) Notify(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { + s.notifyFunc(parent, child) +} diff --git a/internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper_test.go b/internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper_test.go new file mode 100644 index 00000000..b0787a0d --- /dev/null +++ b/internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper_test.go @@ -0,0 +1,154 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package symmetricsecrethelper + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1" +) + +const keyWith32Bytes = "0123456789abcdef0123456789abcdef" + +func TestHelper(t *testing.T) { + labels := map[string]string{ + "some-label-key-1": "some-label-value-1", + "some-label-key-2": "some-label-value-2", + } + randSource := strings.NewReader(keyWith32Bytes) + var notifyParent *configv1alpha1.OIDCProvider + var notifyChild *corev1.Secret + h := New("some-name-prefix-", labels, randSource, func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { + require.True(t, notifyParent == nil && notifyChild == nil, "expected notify func not to have been called yet") + notifyParent = parent + notifyChild = child + }) + + parent := &configv1alpha1.OIDCProvider{ + ObjectMeta: metav1.ObjectMeta{ + UID: "some-uid", + Namespace: "some-namespace", + }, + } + child, err := h.Generate(parent) + require.NoError(t, err) + require.Equal(t, child, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-name-prefix-some-uid", + Namespace: "some-namespace", + Labels: labels, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(parent, schema.GroupVersionKind{ + Group: configv1alpha1.SchemeGroupVersion.Group, + Version: configv1alpha1.SchemeGroupVersion.Version, + Kind: "OIDCProvider", + }), + }, + }, + Type: "secrets.pinniped.dev/symmetric", + Data: map[string][]byte{ + "key": []byte(keyWith32Bytes), + }, + }) + + require.True(t, h.IsValid(parent, child)) + + h.Notify(parent, child) + require.Equal(t, parent, notifyParent) + require.Equal(t, child, notifyChild) +} + +func TestHelperIsValid(t *testing.T) { + tests := []struct { + name string + child func(*corev1.Secret) + parent func(*configv1alpha1.OIDCProvider) + want bool + }{ + { + name: "wrong type", + child: func(s *corev1.Secret) { + s.Type = "wrong" + }, + want: false, + }, + { + name: "empty type", + child: func(s *corev1.Secret) { + s.Type = "" + }, + want: false, + }, + { + name: "data key is too short", + child: func(s *corev1.Secret) { + s.Data["key"] = []byte("short") + }, + want: false, + }, + { + name: "data key does not exist", + child: func(s *corev1.Secret) { + delete(s.Data, "key") + }, + want: false, + }, + { + name: "child not owned by parent", + parent: func(op *configv1alpha1.OIDCProvider) { + op.UID = "wrong" + }, + want: false, + }, + { + name: "happy path", + want: true, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + h := New("none of these args matter", nil, nil, nil) + + parent := &configv1alpha1.OIDCProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-parent-name", + Namespace: "some-namespace", + UID: "some-parent-uid", + }, + } + child := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-name-prefix-some-uid", + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(parent, schema.GroupVersionKind{ + Group: configv1alpha1.SchemeGroupVersion.Group, + Version: configv1alpha1.SchemeGroupVersion.Version, + Kind: "OIDCProvider", + }), + }, + }, + Type: "secrets.pinniped.dev/symmetric", + Data: map[string][]byte{ + "key": []byte(keyWith32Bytes), + }, + } + if test.child != nil { + test.child(child) + } + if test.parent != nil { + test.parent(parent) + } + + require.Equalf(t, test.want, h.IsValid(parent, child), "child: %#v", child) + }) + } +} diff --git a/internal/mocks/mocksecrethelper/generate.go b/internal/mocks/mocksecrethelper/generate.go new file mode 100644 index 00000000..31d52cb4 --- /dev/null +++ b/internal/mocks/mocksecrethelper/generate.go @@ -0,0 +1,6 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package mocksecrethelper + +//go:generate go run -v github.com/golang/mock/mockgen -destination=mocksecrethelper.go -package=mocksecrethelper -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/controller/supervisorconfig/generator SecretHelper diff --git a/internal/mocks/mocksecrethelper/mocksecrethelper.go b/internal/mocks/mocksecrethelper/mocksecrethelper.go new file mode 100644 index 00000000..68b185f3 --- /dev/null +++ b/internal/mocks/mocksecrethelper/mocksecrethelper.go @@ -0,0 +1,94 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: go.pinniped.dev/internal/controller/supervisorconfig/generator (interfaces: SecretHelper) + +// Package mocksecrethelper is a generated GoMock package. +package mocksecrethelper + +import ( + gomock "github.com/golang/mock/gomock" + v1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1" + v1 "k8s.io/api/core/v1" + reflect "reflect" +) + +// MockSecretHelper is a mock of SecretHelper interface +type MockSecretHelper struct { + ctrl *gomock.Controller + recorder *MockSecretHelperMockRecorder +} + +// MockSecretHelperMockRecorder is the mock recorder for MockSecretHelper +type MockSecretHelperMockRecorder struct { + mock *MockSecretHelper +} + +// NewMockSecretHelper creates a new mock instance +func NewMockSecretHelper(ctrl *gomock.Controller) *MockSecretHelper { + mock := &MockSecretHelper{ctrl: ctrl} + mock.recorder = &MockSecretHelperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockSecretHelper) EXPECT() *MockSecretHelperMockRecorder { + return m.recorder +} + +// Generate mocks base method +func (m *MockSecretHelper) Generate(arg0 *v1alpha1.OIDCProvider) (*v1.Secret, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Generate", arg0) + ret0, _ := ret[0].(*v1.Secret) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Generate indicates an expected call of Generate +func (mr *MockSecretHelperMockRecorder) Generate(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Generate", reflect.TypeOf((*MockSecretHelper)(nil).Generate), arg0) +} + +// IsValid mocks base method +func (m *MockSecretHelper) IsValid(arg0 *v1alpha1.OIDCProvider, arg1 *v1.Secret) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsValid", arg0, arg1) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsValid indicates an expected call of IsValid +func (mr *MockSecretHelperMockRecorder) IsValid(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsValid", reflect.TypeOf((*MockSecretHelper)(nil).IsValid), arg0, arg1) +} + +// Name mocks base method +func (m *MockSecretHelper) Name() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Name") + ret0, _ := ret[0].(string) + return ret0 +} + +// Name indicates an expected call of Name +func (mr *MockSecretHelperMockRecorder) Name() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockSecretHelper)(nil).Name)) +} + +// Notify mocks base method +func (m *MockSecretHelper) Notify(arg0 *v1alpha1.OIDCProvider, arg1 *v1.Secret) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Notify", arg0, arg1) +} + +// Notify indicates an expected call of Notify +func (mr *MockSecretHelperMockRecorder) Notify(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Notify", reflect.TypeOf((*MockSecretHelper)(nil).Notify), arg0, arg1) +} diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index 6043ba7c..d16ed682 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -86,14 +86,6 @@ func (m *Manager) SetProviders(oidcProviders ...*provider.OIDCProvider) { for _, incomingProvider := range oidcProviders { providerCache := m.cache.GetOIDCProviderCacheFor(incomingProvider.Issuer()) - if providerCache == nil { // TODO remove when populated from `Secret` values - providerCache = &secret.OIDCProviderCache{} - providerCache.SetTokenHMACKey([]byte("some secret - must have at least 32 bytes")) // TODO fetch from `Secret` - providerCache.SetStateEncoderHashKey([]byte("fake-state-hash-secret")) // TODO fetch from `Secret` - providerCache.SetStateEncoderBlockKey([]byte("16-bytes-STATE01")) // TODO fetch from `Secret` - m.cache.SetOIDCProviderCacheFor(incomingProvider.Issuer(), providerCache) - } - issuer := incomingProvider.Issuer() issuerHostWithPath := strings.ToLower(incomingProvider.IssuerHost()) + "/" + incomingProvider.IssuerPath() oidcTimeouts := oidc.DefaultOIDCTimeoutsConfiguration() diff --git a/internal/oidc/provider/manager/manager_test.go b/internal/oidc/provider/manager/manager_test.go index 0a59abb0..78b2cfd2 100644 --- a/internal/oidc/provider/manager/manager_test.go +++ b/internal/oidc/provider/manager/manager_test.go @@ -246,6 +246,16 @@ func TestManager(t *testing.T) { cache := secret.Cache{} cache.SetCSRFCookieEncoderHashKey([]byte("fake-csrf-hash-secret")) + oidcProvider1Cache := cache.GetOIDCProviderCacheFor(issuer1) + oidcProvider1Cache.SetStateEncoderHashKey([]byte("some-state-encoder-hash-key-1")) + oidcProvider1Cache.SetStateEncoderBlockKey([]byte("16-bytes-STATE01")) + oidcProvider1Cache.SetTokenHMACKey([]byte("some secret 1 - must have at least 32 bytes")) + + oidcProvider2Cache := cache.GetOIDCProviderCacheFor(issuer2) + oidcProvider2Cache.SetStateEncoderHashKey([]byte("some-state-encoder-hash-key-2")) + oidcProvider2Cache.SetStateEncoderBlockKey([]byte("16-bytes-STATE02")) + oidcProvider2Cache.SetTokenHMACKey([]byte("some secret 2 - must have at least 32 bytes")) + subject = NewManager(nextHandler, dynamicJWKSProvider, idpListGetter, &cache, secretsClient) }) @@ -309,21 +319,26 @@ func TestManager(t *testing.T) { requireAuthorizationRequestToBeHandled(issuer2, authRequestParams, upstreamIDPAuthorizationURL) // Hostnames are case-insensitive, so test that we can handle that. - requireAuthorizationRequestToBeHandled(issuer1DifferentCaseHostname, authRequestParams, upstreamIDPAuthorizationURL) - csrfCookieValue, upstreamStateParam := + csrfCookieValue1, upstreamStateParam1 := + requireAuthorizationRequestToBeHandled(issuer1DifferentCaseHostname, authRequestParams, upstreamIDPAuthorizationURL) + csrfCookieValue2, upstreamStateParam2 := requireAuthorizationRequestToBeHandled(issuer2DifferentCaseHostname, authRequestParams, upstreamIDPAuthorizationURL) - callbackRequestParams := "?" + url.Values{ + callbackRequestParams1 := "?" + url.Values{ "code": []string{"some-fake-code"}, - "state": []string{upstreamStateParam}, + "state": []string{upstreamStateParam1}, + }.Encode() + callbackRequestParams2 := "?" + url.Values{ + "code": []string{"some-fake-code"}, + "state": []string{upstreamStateParam2}, }.Encode() - downstreamAuthCode1 := requireCallbackRequestToBeHandled(issuer1, callbackRequestParams, csrfCookieValue) - downstreamAuthCode2 := requireCallbackRequestToBeHandled(issuer2, callbackRequestParams, csrfCookieValue) + downstreamAuthCode1 := requireCallbackRequestToBeHandled(issuer1, callbackRequestParams1, csrfCookieValue1) + downstreamAuthCode2 := requireCallbackRequestToBeHandled(issuer2, callbackRequestParams2, csrfCookieValue2) // Hostnames are case-insensitive, so test that we can handle that. - downstreamAuthCode3 := requireCallbackRequestToBeHandled(issuer1DifferentCaseHostname, callbackRequestParams, csrfCookieValue) - downstreamAuthCode4 := requireCallbackRequestToBeHandled(issuer2DifferentCaseHostname, callbackRequestParams, csrfCookieValue) + downstreamAuthCode3 := requireCallbackRequestToBeHandled(issuer1DifferentCaseHostname, callbackRequestParams1, csrfCookieValue1) + downstreamAuthCode4 := requireCallbackRequestToBeHandled(issuer2DifferentCaseHostname, callbackRequestParams2, csrfCookieValue2) requireTokenRequestToBeHandled(issuer1, downstreamAuthCode1, issuer1JWKS, issuer1) requireTokenRequestToBeHandled(issuer2, downstreamAuthCode2, issuer2JWKS, issuer2) diff --git a/internal/secret/cache.go b/internal/secret/cache.go index 96ccae4a..e8b39965 100644 --- a/internal/secret/cache.go +++ b/internal/secret/cache.go @@ -3,6 +3,9 @@ package secret +// TODO: synchronize me. +// TODO: use SetIssuerXXX() functions instead of returning a struct so that we don't have to worry about reentrancy. + type Cache struct { csrfCookieEncoderHashKey []byte csrfCookieEncoderBlockKey []byte @@ -26,7 +29,12 @@ func (c *Cache) SetCSRFCookieEncoderBlockKey(key []byte) { } func (c *Cache) GetOIDCProviderCacheFor(oidcIssuer string) *OIDCProviderCache { - return c.oidcProviderCaches()[oidcIssuer] + oidcProvider, ok := c.oidcProviderCaches()[oidcIssuer] + if !ok { + oidcProvider = &OIDCProviderCache{} + c.oidcProviderCaches()[oidcIssuer] = oidcProvider + } + return oidcProvider } func (c *Cache) SetOIDCProviderCacheFor(oidcIssuer string, oidcProviderCache *OIDCProviderCache) { From e3ea141bf3831bd6486d64560b58736585122088 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Mon, 14 Dec 2020 10:37:27 -0500 Subject: [PATCH 22/51] Reuse helper filter in generic secret gen controller Signed-off-by: Andrew Keesler --- .../generator/oidc_provider_secrets.go | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go index df3fad84..36c79c26 100644 --- a/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go +++ b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go @@ -70,23 +70,16 @@ func NewOIDCProviderSecretsController( // TODO: de-dup me (jwks_writer.go). withInformer( secretInformer, - controllerlib.FilterFuncs{ - ParentFunc: func(obj metav1.Object) controllerlib.Key { - if isOPCControllee(obj) { - controller := metav1.GetControllerOf(obj) - return controllerlib.Key{ - Name: controller.Name, - Namespace: obj.GetNamespace(), - } + pinnipedcontroller.SimpleFilter(isOPCControllee, func(obj metav1.Object) controllerlib.Key { + if isOPCControllee(obj) { + controller := metav1.GetControllerOf(obj) + return controllerlib.Key{ + Name: controller.Name, + Namespace: obj.GetNamespace(), } - return controllerlib.Key{} - }, - AddFunc: isOPCControllee, - UpdateFunc: func(oldObj, newObj metav1.Object) bool { - return isOPCControllee(oldObj) || isOPCControllee(newObj) - }, - DeleteFunc: isOPCControllee, - }, + } + return controllerlib.Key{} + }), controllerlib.InformerOption{}, ), // We want to be notified when anything happens to an OPC. From 2f28d2a96b24d0c7a9ecbc942f71109674e04e2f Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Mon, 14 Dec 2020 11:32:11 -0500 Subject: [PATCH 23/51] Synchronize the OIDCProvider secrets cache Signed-off-by: Andrew Keesler --- cmd/pinniped-supervisor/main.go | 9 +- internal/oidc/provider/manager/manager.go | 28 +++-- .../oidc/provider/manager/manager_test.go | 14 +-- internal/secret/cache.go | 100 ++++++++--------- internal/secret/cache_test.go | 106 ++++++++++++++++++ 5 files changed, 178 insertions(+), 79 deletions(-) create mode 100644 internal/secret/cache_test.go diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index eb2fe2c4..43b3fa48 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -158,8 +158,7 @@ func startControllers( rand.Reader, func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { plog.Debug("setting hmac secret", "issuer", parent.Spec.Issuer) - secretCache.GetOIDCProviderCacheFor(parent.Spec.Issuer). - SetTokenHMACKey(child.Data[symmetricsecrethelper.SecretDataKey]) + secretCache.SetTokenHMACKey(parent.Spec.Issuer, child.Data[symmetricsecrethelper.SecretDataKey]) }, ), kubeClient, @@ -177,8 +176,7 @@ func startControllers( rand.Reader, func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { plog.Debug("setting state signature key", "issuer", parent.Spec.Issuer) - secretCache.GetOIDCProviderCacheFor(parent.Spec.Issuer). - SetStateEncoderHashKey(child.Data[symmetricsecrethelper.SecretDataKey]) + secretCache.SetStateEncoderHashKey(parent.Spec.Issuer, child.Data[symmetricsecrethelper.SecretDataKey]) }, ), kubeClient, @@ -196,8 +194,7 @@ func startControllers( rand.Reader, func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { plog.Debug("setting state encryption key", "issuer", parent.Spec.Issuer) - secretCache.GetOIDCProviderCacheFor(parent.Spec.Issuer). - SetStateEncoderHashKey(child.Data[symmetricsecrethelper.SecretDataKey]) + secretCache.SetStateEncoderBlockKey(parent.Spec.Issuer, child.Data[symmetricsecrethelper.SecretDataKey]) }, ), kubeClient, diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index d16ed682..fd442765 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -37,7 +37,7 @@ type Manager struct { nextHandler http.Handler // the next handler in a chain, called when this manager didn't know how to handle a request dynamicJWKSProvider jwks.DynamicJWKSProvider // in-memory cache of per-issuer JWKS data idpListGetter oidc.IDPListGetter // in-memory cache of upstream IDPs - cache *secret.Cache // in-memory cache of cryptographic material + secretCache *secret.Cache // in-memory cache of cryptographic material secretsClient corev1client.SecretInterface } @@ -49,7 +49,7 @@ func NewManager( nextHandler http.Handler, dynamicJWKSProvider jwks.DynamicJWKSProvider, idpListGetter oidc.IDPListGetter, - cache *secret.Cache, + secretCache *secret.Cache, secretsClient corev1client.SecretInterface, ) *Manager { return &Manager{ @@ -57,7 +57,7 @@ func NewManager( nextHandler: nextHandler, dynamicJWKSProvider: dynamicJWKSProvider, idpListGetter: idpListGetter, - cache: cache, + secretCache: secretCache, secretsClient: secretsClient, } } @@ -79,28 +79,28 @@ func (m *Manager) SetProviders(oidcProviders ...*provider.OIDCProvider) { var csrfCookieEncoder = dynamiccodec.New( oidc.CSRFCookieLifespan, - m.cache.GetCSRFCookieEncoderHashKey, - m.cache.GetCSRFCookieEncoderBlockKey, + m.secretCache.GetCSRFCookieEncoderHashKey, + func() []byte { return nil }, ) for _, incomingProvider := range oidcProviders { - providerCache := m.cache.GetOIDCProviderCacheFor(incomingProvider.Issuer()) - issuer := incomingProvider.Issuer() issuerHostWithPath := strings.ToLower(incomingProvider.IssuerHost()) + "/" + incomingProvider.IssuerPath() oidcTimeouts := oidc.DefaultOIDCTimeoutsConfiguration() + tokenHMACKeyGetter := wrapGetter(incomingProvider.Issuer(), m.secretCache.GetTokenHMACKey) + // Use NullStorage for the authorize endpoint because we do not actually want to store anything until // the upstream callback endpoint is called later. - oauthHelperWithNullStorage := oidc.FositeOauth2Helper(oidc.NullStorage{}, issuer, providerCache.GetTokenHMACKey, nil, oidcTimeouts) + oauthHelperWithNullStorage := oidc.FositeOauth2Helper(oidc.NullStorage{}, issuer, tokenHMACKeyGetter, nil, oidcTimeouts) // For all the other endpoints, make another oauth helper with exactly the same settings except use real storage. - oauthHelperWithKubeStorage := oidc.FositeOauth2Helper(oidc.NewKubeStorage(m.secretsClient), issuer, providerCache.GetTokenHMACKey, m.dynamicJWKSProvider, oidcTimeouts) + oauthHelperWithKubeStorage := oidc.FositeOauth2Helper(oidc.NewKubeStorage(m.secretsClient), issuer, tokenHMACKeyGetter, m.dynamicJWKSProvider, oidcTimeouts) var upstreamStateEncoder = dynamiccodec.New( oidcTimeouts.UpstreamStateParamLifespan, - providerCache.GetStateEncoderHashKey, - providerCache.GetStateEncoderBlockKey, + wrapGetter(incomingProvider.Issuer(), m.secretCache.GetStateEncoderHashKey), + wrapGetter(incomingProvider.Issuer(), m.secretCache.GetStateEncoderBlockKey), ) m.providerHandlers[(issuerHostWithPath + oidc.WellKnownEndpointPath)] = discovery.NewHandler(issuer) @@ -158,3 +158,9 @@ func (m *Manager) findHandler(req *http.Request) http.Handler { return m.providerHandlers[strings.ToLower(req.Host)+"/"+req.URL.Path] } + +func wrapGetter(issuer string, getter func(string) []byte) func() []byte { + return func() []byte { + return getter(issuer) + } +} diff --git a/internal/oidc/provider/manager/manager_test.go b/internal/oidc/provider/manager/manager_test.go index 78b2cfd2..18ec0036 100644 --- a/internal/oidc/provider/manager/manager_test.go +++ b/internal/oidc/provider/manager/manager_test.go @@ -246,15 +246,13 @@ func TestManager(t *testing.T) { cache := secret.Cache{} cache.SetCSRFCookieEncoderHashKey([]byte("fake-csrf-hash-secret")) - oidcProvider1Cache := cache.GetOIDCProviderCacheFor(issuer1) - oidcProvider1Cache.SetStateEncoderHashKey([]byte("some-state-encoder-hash-key-1")) - oidcProvider1Cache.SetStateEncoderBlockKey([]byte("16-bytes-STATE01")) - oidcProvider1Cache.SetTokenHMACKey([]byte("some secret 1 - must have at least 32 bytes")) + cache.SetTokenHMACKey(issuer1, []byte("some secret 1 - must have at least 32 bytes")) + cache.SetStateEncoderHashKey(issuer1, []byte("some-state-encoder-hash-key-1")) + cache.SetStateEncoderBlockKey(issuer1, []byte("16-bytes-STATE01")) - oidcProvider2Cache := cache.GetOIDCProviderCacheFor(issuer2) - oidcProvider2Cache.SetStateEncoderHashKey([]byte("some-state-encoder-hash-key-2")) - oidcProvider2Cache.SetStateEncoderBlockKey([]byte("16-bytes-STATE02")) - oidcProvider2Cache.SetTokenHMACKey([]byte("some secret 2 - must have at least 32 bytes")) + cache.SetTokenHMACKey(issuer2, []byte("some secret 2 - must have at least 32 bytes")) + cache.SetStateEncoderHashKey(issuer2, []byte("some-state-encoder-hash-key-2")) + cache.SetStateEncoderBlockKey(issuer2, []byte("16-bytes-STATE02")) subject = NewManager(nextHandler, dynamicJWKSProvider, idpListGetter, &cache, secretsClient) }) diff --git a/internal/secret/cache.go b/internal/secret/cache.go index e8b39965..6c0f4063 100644 --- a/internal/secret/cache.go +++ b/internal/secret/cache.go @@ -3,77 +3,69 @@ package secret -// TODO: synchronize me. -// TODO: use SetIssuerXXX() functions instead of returning a struct so that we don't have to worry about reentrancy. +import ( + "sync" + "sync/atomic" +) type Cache struct { - csrfCookieEncoderHashKey []byte - csrfCookieEncoderBlockKey []byte - oidcProviderCacheMap map[string]*OIDCProviderCache + csrfCookieEncoderHashKey atomic.Value + oidcProviderCacheMap sync.Map +} + +// New returns an empty Cache. +func New() *Cache { return &Cache{} } + +type oidcProviderCache struct { + tokenHMACKey atomic.Value + stateEncoderHashKey atomic.Value + stateEncoderBlockKey atomic.Value } func (c *Cache) GetCSRFCookieEncoderHashKey() []byte { - return c.csrfCookieEncoderHashKey + return bytesOrNil(c.csrfCookieEncoderHashKey.Load()) } func (c *Cache) SetCSRFCookieEncoderHashKey(key []byte) { - c.csrfCookieEncoderHashKey = key + c.csrfCookieEncoderHashKey.Store(key) } -func (c *Cache) GetCSRFCookieEncoderBlockKey() []byte { - return c.csrfCookieEncoderBlockKey +func (c *Cache) GetTokenHMACKey(oidcIssuer string) []byte { + return bytesOrNil(c.getOIDCProviderCache(oidcIssuer).tokenHMACKey.Load()) } -func (c *Cache) SetCSRFCookieEncoderBlockKey(key []byte) { - c.csrfCookieEncoderBlockKey = key +func (c *Cache) SetTokenHMACKey(oidcIssuer string, key []byte) { + c.getOIDCProviderCache(oidcIssuer).tokenHMACKey.Store(key) } -func (c *Cache) GetOIDCProviderCacheFor(oidcIssuer string) *OIDCProviderCache { - oidcProvider, ok := c.oidcProviderCaches()[oidcIssuer] +func (c *Cache) GetStateEncoderHashKey(oidcIssuer string) []byte { + return bytesOrNil(c.getOIDCProviderCache(oidcIssuer).stateEncoderHashKey.Load()) +} + +func (c *Cache) SetStateEncoderHashKey(oidcIssuer string, key []byte) { + c.getOIDCProviderCache(oidcIssuer).stateEncoderHashKey.Store(key) +} + +func (c *Cache) GetStateEncoderBlockKey(oidcIssuer string) []byte { + return bytesOrNil(c.getOIDCProviderCache(oidcIssuer).stateEncoderBlockKey.Load()) +} + +func (c *Cache) SetStateEncoderBlockKey(oidcIssuer string, key []byte) { + c.getOIDCProviderCache(oidcIssuer).stateEncoderBlockKey.Store(key) +} + +func (c *Cache) getOIDCProviderCache(oidcIssuer string) *oidcProviderCache { + value, ok := c.oidcProviderCacheMap.Load(oidcIssuer) if !ok { - oidcProvider = &OIDCProviderCache{} - c.oidcProviderCaches()[oidcIssuer] = oidcProvider + value = &oidcProviderCache{} + c.oidcProviderCacheMap.Store(oidcIssuer, value) } - return oidcProvider + return value.(*oidcProviderCache) } -func (c *Cache) SetOIDCProviderCacheFor(oidcIssuer string, oidcProviderCache *OIDCProviderCache) { - c.oidcProviderCaches()[oidcIssuer] = oidcProviderCache -} - -func (c *Cache) oidcProviderCaches() map[string]*OIDCProviderCache { - if c.oidcProviderCacheMap == nil { - c.oidcProviderCacheMap = map[string]*OIDCProviderCache{} +func bytesOrNil(b interface{}) []byte { + if b == nil { + return nil } - return c.oidcProviderCacheMap -} - -type OIDCProviderCache struct { - tokenHMACKey []byte - stateEncoderHashKey []byte - stateEncoderBlockKey []byte -} - -func (o *OIDCProviderCache) GetTokenHMACKey() []byte { - return o.tokenHMACKey -} - -func (o *OIDCProviderCache) SetTokenHMACKey(key []byte) { - o.tokenHMACKey = key -} - -func (o *OIDCProviderCache) GetStateEncoderHashKey() []byte { - return o.stateEncoderHashKey -} - -func (o *OIDCProviderCache) SetStateEncoderHashKey(key []byte) { - o.stateEncoderHashKey = key -} - -func (o *OIDCProviderCache) GetStateEncoderBlockKey() []byte { - return o.stateEncoderBlockKey -} - -func (o *OIDCProviderCache) SetStateEncoderBlockKey(key []byte) { - o.stateEncoderBlockKey = key + return b.([]byte) } diff --git a/internal/secret/cache_test.go b/internal/secret/cache_test.go new file mode 100644 index 00000000..40fdf612 --- /dev/null +++ b/internal/secret/cache_test.go @@ -0,0 +1,106 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package secret + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" +) + +const ( + issuer = "some-issuer" + otherIssuer = "other-issuer" +) + +var ( + csrfCookieEncoderHashKey = []byte("csrf-cookie-encoder-hash-key") + tokenHMACKey = []byte("token-hmac-key") + stateEncoderHashKey = []byte("state-encoder-hash-key") + otherStateEncoderHashKey = []byte("other-state-encoder-hash-key") + stateEncoderBlockKey = []byte("state-encoder-block-key") +) + +func TestCache(t *testing.T) { + c := New() + + // Validate we get a nil return value when stuff does not exist. + require.Nil(t, c.GetCSRFCookieEncoderHashKey()) + require.Nil(t, c.GetTokenHMACKey(issuer)) + require.Nil(t, c.GetStateEncoderHashKey(issuer)) + require.Nil(t, c.GetStateEncoderBlockKey(issuer)) + + // Validate we get some nil and non-nil values when some stuff exists. + c.SetCSRFCookieEncoderHashKey(csrfCookieEncoderHashKey) + require.Equal(t, csrfCookieEncoderHashKey, c.GetCSRFCookieEncoderHashKey()) + require.Nil(t, c.GetTokenHMACKey(issuer)) + c.SetStateEncoderHashKey(issuer, stateEncoderHashKey) + require.Equal(t, stateEncoderHashKey, c.GetStateEncoderHashKey(issuer)) + require.Nil(t, c.GetStateEncoderBlockKey(issuer)) + + // Validate we get non-nil values when all stuff exists. + c.SetCSRFCookieEncoderHashKey(csrfCookieEncoderHashKey) + c.SetTokenHMACKey(issuer, tokenHMACKey) + c.SetStateEncoderHashKey(issuer, otherStateEncoderHashKey) + c.SetStateEncoderBlockKey(issuer, stateEncoderBlockKey) + require.Equal(t, csrfCookieEncoderHashKey, c.GetCSRFCookieEncoderHashKey()) + require.Equal(t, tokenHMACKey, c.GetTokenHMACKey(issuer)) + require.Equal(t, otherStateEncoderHashKey, c.GetStateEncoderHashKey(issuer)) + require.Equal(t, stateEncoderBlockKey, c.GetStateEncoderBlockKey(issuer)) + + // Validate that stuff is still nil for an unknown issuer. + require.Nil(t, c.GetTokenHMACKey(otherIssuer)) + require.Nil(t, c.GetStateEncoderHashKey(otherIssuer)) + require.Nil(t, c.GetStateEncoderBlockKey(otherIssuer)) +} + +// TestCacheSynchronized should mimic the behavior of an OIDCProvider: multiple goroutines +// read the same fields, sequentially, from the cache. +func TestCacheSynchronized(t *testing.T) { + c := New() + + c.SetCSRFCookieEncoderHashKey(csrfCookieEncoderHashKey) + c.SetTokenHMACKey(issuer, tokenHMACKey) + c.SetStateEncoderHashKey(issuer, stateEncoderHashKey) + c.SetStateEncoderBlockKey(issuer, stateEncoderBlockKey) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + + eg, _ := errgroup.WithContext(ctx) + + eg.Go(func() error { + for i := 0; i < 100; i++ { + require.Equal(t, csrfCookieEncoderHashKey, c.GetCSRFCookieEncoderHashKey()) + require.Equal(t, tokenHMACKey, c.GetTokenHMACKey(issuer)) + require.Equal(t, stateEncoderHashKey, c.GetStateEncoderHashKey(issuer)) + require.Equal(t, stateEncoderBlockKey, c.GetStateEncoderBlockKey(issuer)) + } + return nil + }) + + eg.Go(func() error { + for i := 0; i < 100; i++ { + require.Equal(t, csrfCookieEncoderHashKey, c.GetCSRFCookieEncoderHashKey()) + require.Equal(t, tokenHMACKey, c.GetTokenHMACKey(issuer)) + require.Equal(t, stateEncoderHashKey, c.GetStateEncoderHashKey(issuer)) + require.Equal(t, stateEncoderBlockKey, c.GetStateEncoderBlockKey(issuer)) + } + return nil + }) + + eg.Go(func() error { + for i := 0; i < 100; i++ { + require.Nil(t, c.GetTokenHMACKey(otherIssuer)) + require.Nil(t, c.GetStateEncoderHashKey(otherIssuer)) + require.Nil(t, c.GetStateEncoderBlockKey(otherIssuer)) + } + return nil + }) + + require.NoError(t, eg.Wait()) +} From 5b7a86ecc1b4de15d7c9d3dbffcdc5a8a5667a9e Mon Sep 17 00:00:00 2001 From: Aram Price Date: Mon, 14 Dec 2020 15:53:12 -0500 Subject: [PATCH 24/51] Integration test for Supervisor secret controllers This forced us to add labels to the CSRF cookie secret, just as we do for other Supervisor secrets. Yay tests. Signed-off-by: Andrew Keesler --- cmd/pinniped-supervisor/main.go | 1 + .../generator/supervisor_secrets.go | 9 +- .../generator/supervisor_secrets_test.go | 10 +- test/integration/supervisor_keys_test.go | 101 ----------- test/integration/supervisor_secrets_test.go | 166 ++++++++++++++++++ 5 files changed, 181 insertions(+), 106 deletions(-) delete mode 100644 test/integration/supervisor_keys_test.go create mode 100644 test/integration/supervisor_secrets_test.go diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index 26cfb32c..0f2a9665 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -152,6 +152,7 @@ func startControllers( WithController( generator.NewSupervisorSecretsController( supervisorDeployment, + cfg.Labels, kubeClient, secretInformer, func(secret []byte) { diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets.go b/internal/controller/supervisorconfig/generator/supervisor_secrets.go index cc3a9d33..f95379ce 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets.go @@ -46,6 +46,7 @@ func generateSymmetricKey() ([]byte, error) { type supervisorSecretsController struct { owner *appsv1.Deployment + labels map[string]string client kubernetes.Interface secrets corev1informers.SecretInformer setCache func(secret []byte) @@ -53,16 +54,17 @@ type supervisorSecretsController struct { // NewSupervisorSecretsController instantiates a new controllerlib.Controller which will ensure existence of a generated secret. func NewSupervisorSecretsController( - // TODO: label the generated secret like we do in the JWKSWriterController // TODO: generate the name for the secret and label the secret with the UID of the owner? So that we don't have naming conflicts if the user has already created a Secret with that name. // TODO: add tests for the filter like we do in the JWKSWriterController? owner *appsv1.Deployment, + labels map[string]string, client kubernetes.Interface, secrets corev1informers.SecretInformer, setCache func(secret []byte), ) controllerlib.Controller { c := supervisorSecretsController{ owner: owner, + labels: labels, client: client, secrets: secrets, setCache: setCache, @@ -95,7 +97,7 @@ func (c *supervisorSecretsController) Sync(ctx controllerlib.Context) error { return nil } - newSecret, err := generateSecret(ctx.Key.Namespace, ctx.Key.Name, secretDataFunc, c.owner) + newSecret, err := generateSecret(ctx.Key.Namespace, ctx.Key.Name, c.labels, secretDataFunc, c.owner) if err != nil { return fmt.Errorf("failed to generate secret: %w", err) } @@ -141,7 +143,7 @@ func secretDataFunc() (map[string][]byte, error) { }, nil } -func generateSecret(namespace, name string, secretDataFunc func() (map[string][]byte, error), owner metav1.Object) (*corev1.Secret, error) { +func generateSecret(namespace, name string, labels map[string]string, secretDataFunc func() (map[string][]byte, error), owner metav1.Object) (*corev1.Secret, error) { secretData, err := secretDataFunc() if err != nil { return nil, err @@ -159,6 +161,7 @@ func generateSecret(namespace, name string, secretDataFunc func() (map[string][] OwnerReferences: []metav1.OwnerReference{ *metav1.NewControllerRef(owner, deploymentGVK), }, + Labels: labels, }, Type: symmetricKeySecretType, Data: secretData, diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go b/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go index f782cb5c..e3ae8905 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go @@ -28,7 +28,6 @@ func TestController(t *testing.T) { const ( generatedSecretNamespace = "some-namespace" generatedSecretName = "some-name-abc123" - otherGeneratedSecretName = "some-other-name-abc123" ) var ( @@ -53,6 +52,11 @@ func TestController(t *testing.T) { generatedSymmetricKey = []byte("some-neato-32-byte-generated-key") otherGeneratedSymmetricKey = []byte("some-funio-32-byte-generated-key") + labels = map[string]string{ + "some-label-key-1": "some-label-value-1", + "some-label-key-2": "some-label-value-2", + } + generatedSecret = &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: generatedSecretName, @@ -60,6 +64,7 @@ func TestController(t *testing.T) { OwnerReferences: []metav1.OwnerReference{ *metav1.NewControllerRef(owner, ownerGVK), }, + Labels: labels, }, Type: "secrets.pinniped.dev/symmetric", Data: map[string][]byte{ @@ -74,6 +79,7 @@ func TestController(t *testing.T) { OwnerReferences: []metav1.OwnerReference{ *metav1.NewControllerRef(owner, ownerGVK), }, + Labels: labels, }, Type: "secrets.pinniped.dev/symmetric", Data: map[string][]byte{ @@ -307,7 +313,7 @@ func TestController(t *testing.T) { secrets := informers.Core().V1().Secrets() var callbackSecret []byte - c := NewSupervisorSecretsController(owner, apiClient, secrets, func(secret []byte) { + c := NewSupervisorSecretsController(owner, labels, apiClient, secrets, func(secret []byte) { require.Nil(t, callbackSecret, "callback was called twice") callbackSecret = secret }) diff --git a/test/integration/supervisor_keys_test.go b/test/integration/supervisor_keys_test.go deleted file mode 100644 index d59c713e..00000000 --- a/test/integration/supervisor_keys_test.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package integration - -import ( - "context" - "encoding/json" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/square/go-jose.v2" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1" - "go.pinniped.dev/test/library" -) - -func TestSupervisorOIDCKeys(t *testing.T) { - env := library.IntegrationEnv(t) - kubeClient := library.NewClientset(t) - supervisorClient := library.NewSupervisorClientset(t) - - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancel() - - // Create our OPC under test. - opc := library.CreateTestOIDCProvider(ctx, t, "", "", "") - - // Ensure a secret is created with the OPC's JWKS. - var updatedOPC *configv1alpha1.OIDCProvider - var err error - assert.Eventually(t, func() bool { - updatedOPC, err = supervisorClient. - ConfigV1alpha1(). - OIDCProviders(env.SupervisorNamespace). - Get(ctx, opc.Name, metav1.GetOptions{}) - return err == nil && updatedOPC.Status.JWKSSecret.Name != "" - }, time.Second*10, time.Millisecond*500) - require.NoError(t, err) - require.NotEmpty(t, updatedOPC.Status.JWKSSecret.Name) - - // Ensure the secret actually exists. - secret, err := kubeClient. - CoreV1(). - Secrets(env.SupervisorNamespace). - Get(ctx, updatedOPC.Status.JWKSSecret.Name, 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 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) - - // Ensure upon deleting the secret, it is eventually brought back. - err = kubeClient. - CoreV1(). - Secrets(env.SupervisorNamespace). - Delete(ctx, updatedOPC.Status.JWKSSecret.Name, metav1.DeleteOptions{}) - require.NoError(t, err) - assert.Eventually(t, func() bool { - secret, err = kubeClient. - CoreV1(). - Secrets(env.SupervisorNamespace). - Get(ctx, updatedOPC.Status.JWKSSecret.Name, metav1.GetOptions{}) - return err == nil - }, time.Second*10, time.Millisecond*500) - require.NoError(t, err) - - // Upon deleting the OPC, the secret is deleted (we test this behavior in our uninstall tests). -} diff --git a/test/integration/supervisor_secrets_test.go b/test/integration/supervisor_secrets_test.go new file mode 100644 index 00000000..9c3f8c0f --- /dev/null +++ b/test/integration/supervisor_secrets_test.go @@ -0,0 +1,166 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package integration + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "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/1.19/apis/supervisor/config/v1alpha1" + "go.pinniped.dev/test/library" +) + +func TestSupervisorSecrets(t *testing.T) { + env := library.IntegrationEnv(t) + kubeClient := library.NewClientset(t) + supervisorClient := library.NewSupervisorClientset(t) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + // Create our OP under test. + op := library.CreateTestOIDCProvider(ctx, t, "", "", "") + + tests := []struct { + name string + secretName func(op *configv1alpha1.OIDCProvider) string + ensureValid func(t *testing.T, secret *corev1.Secret) + }{ + { + name: "csrf cookie signing key", + secretName: func(op *configv1alpha1.OIDCProvider) string { + return env.SupervisorAppName + "-key" + }, + ensureValid: ensureValidSymmetricKey, + }, + { + name: "jwks", + secretName: func(op *configv1alpha1.OIDCProvider) string { + return op.Status.JWKSSecret.Name + }, + ensureValid: ensureValidJWKS, + }, + { + name: "hmac signing secret", + secretName: func(op *configv1alpha1.OIDCProvider) string { + return "pinniped-oidc-provider-hmac-key-" + string(op.UID) + }, + ensureValid: ensureValidSymmetricKey, + }, + { + name: "state signature secret", + secretName: func(op *configv1alpha1.OIDCProvider) string { + return "pinniped-oidc-provider-upstream-state-signature-key-" + string(op.UID) + }, + ensureValid: ensureValidSymmetricKey, + }, + { + name: "state encryption secret", + secretName: func(op *configv1alpha1.OIDCProvider) string { + return "pinniped-oidc-provider-upstream-state-encryption-key-" + string(op.UID) + }, + ensureValid: ensureValidSymmetricKey, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + // Ensure a secret is created with the OP's JWKS. + var updatedOP *configv1alpha1.OIDCProvider + var err error + assert.Eventually(t, func() bool { + updatedOP, err = supervisorClient. + ConfigV1alpha1(). + OIDCProviders(env.SupervisorNamespace). + Get(ctx, op.Name, metav1.GetOptions{}) + return err == nil && test.secretName(updatedOP) != "" + }, time.Second*10, time.Millisecond*500) + require.NoError(t, err) + require.NotEmpty(t, test.secretName(updatedOP)) + + // Ensure the secret actually exists. + secret, err := kubeClient. + CoreV1(). + Secrets(env.SupervisorNamespace). + Get(ctx, test.secretName(updatedOP), 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(updatedOP), metav1.DeleteOptions{}) + require.NoError(t, err) + assert.Eventually(t, func() bool { + secret, err = kubeClient. + CoreV1(). + Secrets(env.SupervisorNamespace). + Get(ctx, test.secretName(updatedOP), metav1.GetOptions{}) + return err == nil + }, time.Second*10, time.Millisecond*500) + require.NoError(t, err) + + // Ensure that the new secret is valid. + test.ensureValid(t, secret) + }) + } + + // Upon deleting the OP, 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 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 ensureValidSymmetricKey(t *testing.T, secret *corev1.Secret) { + t.Helper() + require.Equal(t, corev1.SecretType("secrets.pinniped.dev/symmetric"), 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)) +} From 9c79adcb26e7ec4d86fed97e80ec57e605432e83 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Mon, 14 Dec 2020 14:24:13 -0800 Subject: [PATCH 25/51] Rename and move some code to perpare for refactor Signed-off-by: aram price --- .../supervisorconfig/generator/generator.go | 92 +++++++++++++++++++ .../generator/oidc_provider_secrets.go | 17 +--- .../generator/supervisor_secrets.go | 70 -------------- 3 files changed, 94 insertions(+), 85 deletions(-) create mode 100644 internal/controller/supervisorconfig/generator/generator.go diff --git a/internal/controller/supervisorconfig/generator/generator.go b/internal/controller/supervisorconfig/generator/generator.go new file mode 100644 index 00000000..bed18e36 --- /dev/null +++ b/internal/controller/supervisorconfig/generator/generator.go @@ -0,0 +1,92 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package generator + +import ( + "crypto/rand" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1" +) + +const ( + symmetricKeySecretType = "secrets.pinniped.dev/symmetric" + symmetricKeySecretDataKey = "key" + + symmetricKeySize = 32 + + opKind = "OIDCProvider" +) + +func generateSymmetricKey() ([]byte, error) { + b := make([]byte, symmetricKeySize) + if _, err := rand.Read(b); err != nil { + return nil, err + } + return b, nil +} + +func isValid(secret *corev1.Secret) bool { + if secret.Type != symmetricKeySecretType { + return false + } + + data, ok := secret.Data[symmetricKeySecretDataKey] + if !ok { + return false + } + if len(data) != symmetricKeySize { + return false + } + + return true +} + +func secretDataFunc() (map[string][]byte, error) { + symmetricKey, err := generateKey() + if err != nil { + return nil, err + } + + return map[string][]byte{ + symmetricKeySecretDataKey: symmetricKey, + }, nil +} + +func generateSecret(namespace, name string, labels map[string]string, secretDataFunc func() (map[string][]byte, error), owner metav1.Object) (*corev1.Secret, error) { + secretData, err := secretDataFunc() + if err != nil { + return nil, err + } + + deploymentGVK := schema.GroupVersionKind{ + Group: appsv1.SchemeGroupVersion.Group, + Version: appsv1.SchemeGroupVersion.Version, + Kind: "Deployment", + } + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(owner, deploymentGVK), + }, + Labels: labels, + }, + Type: symmetricKeySecretType, + Data: secretData, + }, nil +} + +// isOPCControlle returns whether the provided obj is controlled by an OPC. +func isOPControllee(obj metav1.Object) bool { + controller := metav1.GetControllerOf(obj) + return controller != nil && + controller.APIVersion == configv1alpha1.SchemeGroupVersion.String() && + controller.Kind == opKind +} diff --git a/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go index 36c79c26..4069e200 100644 --- a/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go +++ b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go @@ -22,11 +22,6 @@ import ( "go.pinniped.dev/internal/plog" ) -const ( - // TODO should this live on `provider.OIDCProvider` ? - opcKind = "OIDCProvider" // TODO: deduplicate - internal/controller/supervisorconfig/jwks_writer.go -) - // SecretHelper describes an object that can Generate() a Secret and determine whether a Secret // IsValid(). It can also be Notify()'d about a Secret being persisted. // @@ -70,8 +65,8 @@ func NewOIDCProviderSecretsController( // TODO: de-dup me (jwks_writer.go). withInformer( secretInformer, - pinnipedcontroller.SimpleFilter(isOPCControllee, func(obj metav1.Object) controllerlib.Key { - if isOPCControllee(obj) { + pinnipedcontroller.SimpleFilter(isOPControllee, func(obj metav1.Object) controllerlib.Key { + if isOPControllee(obj) { controller := metav1.GetControllerOf(obj) return controllerlib.Key{ Name: controller.Name, @@ -211,11 +206,3 @@ func (c *oidcProviderSecretsController) createOrUpdateSecret( return err }) } - -// isOPCControlle returns whether the provided obj is controlled by an OPC. -func isOPCControllee(obj metav1.Object) bool { // TODO: deduplicate - internal/controller/supervisorconfig/jwks_writer.go - controller := metav1.GetControllerOf(obj) - return controller != nil && - controller.APIVersion == configv1alpha1.SchemeGroupVersion.String() && - controller.Kind == opcKind -} diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets.go b/internal/controller/supervisorconfig/generator/supervisor_secrets.go index f95379ce..61fb0140 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets.go @@ -6,14 +6,12 @@ package generator import ( "context" - "crypto/rand" "fmt" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" corev1informers "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/util/retry" @@ -24,26 +22,10 @@ import ( "go.pinniped.dev/internal/plog" ) -// TODO: de-dup me when we abstract these controllers. -const ( - symmetricKeySecretType = "secrets.pinniped.dev/symmetric" - symmetricKeySecretDataKey = "key" - - symmetricKeySize = 32 // TODO: what should this be? -) - // generateKey is stubbed out for the purpose of testing. The default behavior is to generate a symmetric key. //nolint:gochecknoglobals var generateKey = generateSymmetricKey -func generateSymmetricKey() ([]byte, error) { - b := make([]byte, symmetricKeySize) - if _, err := rand.Read(b); err != nil { - return nil, err - } - return b, nil -} - type supervisorSecretsController struct { owner *appsv1.Deployment labels map[string]string @@ -116,58 +98,6 @@ func (c *supervisorSecretsController) Sync(ctx controllerlib.Context) error { return nil } -func isValid(secret *corev1.Secret) bool { - if secret.Type != symmetricKeySecretType { - return false - } - - data, ok := secret.Data[symmetricKeySecretDataKey] - if !ok { - return false - } - if len(data) != symmetricKeySize { - return false - } - - return true -} - -func secretDataFunc() (map[string][]byte, error) { - symmetricKey, err := generateKey() - if err != nil { - return nil, err - } - - return map[string][]byte{ - symmetricKeySecretDataKey: symmetricKey, - }, nil -} - -func generateSecret(namespace, name string, labels map[string]string, secretDataFunc func() (map[string][]byte, error), owner metav1.Object) (*corev1.Secret, error) { - secretData, err := secretDataFunc() - if err != nil { - return nil, err - } - - deploymentGVK := schema.GroupVersionKind{ - Group: appsv1.SchemeGroupVersion.Group, - Version: appsv1.SchemeGroupVersion.Version, - Kind: "Deployment", - } - return &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - OwnerReferences: []metav1.OwnerReference{ - *metav1.NewControllerRef(owner, deploymentGVK), - }, - Labels: labels, - }, - Type: symmetricKeySecretType, - Data: secretData, - }, nil -} - func (c *supervisorSecretsController) createSecret(ctx context.Context, newSecret *corev1.Secret) error { _, err := c.client.CoreV1().Secrets(newSecret.Namespace).Create(ctx, newSecret, metav1.CreateOptions{}) return err From 6e8d5640135fab6addbee86b7cd0aba2c5d5cd4f Mon Sep 17 00:00:00 2001 From: aram price Date: Mon, 14 Dec 2020 16:08:48 -0800 Subject: [PATCH 26/51] Test filters in SupervisorSecretsController --- cmd/pinniped-supervisor/main.go | 1 + .../generator/supervisor_secrets.go | 49 ++--- .../generator/supervisor_secrets_test.go | 187 +++++++++++++++--- 3 files changed, 192 insertions(+), 45 deletions(-) diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index 0f2a9665..6c87b43e 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -159,6 +159,7 @@ func startControllers( plog.Debug("setting csrf cookie secret") secretCache.SetCSRFCookieEncoderHashKey(secret) }, + controllerlib.WithInformer, ), singletonWorker, ). diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets.go b/internal/controller/supervisorconfig/generator/supervisor_secrets.go index 61fb0140..e27166ae 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets.go @@ -27,36 +27,39 @@ import ( var generateKey = generateSymmetricKey type supervisorSecretsController struct { - owner *appsv1.Deployment - labels map[string]string - client kubernetes.Interface - secrets corev1informers.SecretInformer - setCache func(secret []byte) + owner *appsv1.Deployment + labels map[string]string + kubeClient kubernetes.Interface + secretInformer corev1informers.SecretInformer + setCacheFunc func(secret []byte) } // NewSupervisorSecretsController instantiates a new controllerlib.Controller which will ensure existence of a generated secret. func NewSupervisorSecretsController( // TODO: generate the name for the secret and label the secret with the UID of the owner? So that we don't have naming conflicts if the user has already created a Secret with that name. - // TODO: add tests for the filter like we do in the JWKSWriterController? owner *appsv1.Deployment, labels map[string]string, - client kubernetes.Interface, - secrets corev1informers.SecretInformer, - setCache func(secret []byte), + kubeClient kubernetes.Interface, + secretInformer corev1informers.SecretInformer, + setCacheFunc func(secret []byte), + withInformer pinnipedcontroller.WithInformerOptionFunc, ) controllerlib.Controller { c := supervisorSecretsController{ - owner: owner, - labels: labels, - client: client, - secrets: secrets, - setCache: setCache, + owner: owner, + labels: labels, + kubeClient: kubeClient, + secretInformer: secretInformer, + setCacheFunc: setCacheFunc, } - filter := pinnipedcontroller.SimpleFilter(func(obj metav1.Object) bool { - return metav1.IsControlledBy(obj, owner) - }, nil) return controllerlib.New( controllerlib.Config{Name: owner.Name + "-secret-generator", Syncer: &c}, - controllerlib.WithInformer(secrets, filter, controllerlib.InformerOption{}), + withInformer( + secretInformer, + pinnipedcontroller.SimpleFilter(func(obj metav1.Object) bool { + return metav1.IsControlledBy(obj, owner) + }, nil), + controllerlib.InformerOption{}, + ), controllerlib.WithInitialEvent(controllerlib.Key{ Namespace: owner.Namespace, Name: owner.Name + "-key", @@ -66,7 +69,7 @@ func NewSupervisorSecretsController( // Sync implements controllerlib.Syncer.Sync(). func (c *supervisorSecretsController) Sync(ctx controllerlib.Context) error { - secret, err := c.secrets.Lister().Secrets(ctx.Key.Namespace).Get(ctx.Key.Name) + secret, err := c.secretInformer.Lister().Secrets(ctx.Key.Namespace).Get(ctx.Key.Name) isNotFound := k8serrors.IsNotFound(err) if !isNotFound && err != nil { return fmt.Errorf("failed to list secret %s/%s: %w", ctx.Key.Namespace, ctx.Key.Name, err) @@ -75,7 +78,7 @@ func (c *supervisorSecretsController) Sync(ctx controllerlib.Context) error { secretNeedsUpdate := isNotFound || !isValid(secret) if !secretNeedsUpdate { plog.Debug("secret is up to date", "secret", klog.KObj(secret)) - c.setCache(secret.Data[symmetricKeySecretDataKey]) + c.setCacheFunc(secret.Data[symmetricKeySecretDataKey]) return nil } @@ -93,18 +96,18 @@ func (c *supervisorSecretsController) Sync(ctx controllerlib.Context) error { return fmt.Errorf("failed to create/update secret %s/%s: %w", newSecret.Namespace, newSecret.Name, err) } - c.setCache(newSecret.Data[symmetricKeySecretDataKey]) + c.setCacheFunc(newSecret.Data[symmetricKeySecretDataKey]) return nil } func (c *supervisorSecretsController) createSecret(ctx context.Context, newSecret *corev1.Secret) error { - _, err := c.client.CoreV1().Secrets(newSecret.Namespace).Create(ctx, newSecret, metav1.CreateOptions{}) + _, err := c.kubeClient.CoreV1().Secrets(newSecret.Namespace).Create(ctx, newSecret, metav1.CreateOptions{}) return err } func (c *supervisorSecretsController) updateSecret(ctx context.Context, newSecret **corev1.Secret, secretName string) error { - secrets := c.client.CoreV1().Secrets((*newSecret).Namespace) + secrets := c.kubeClient.CoreV1().Secrets((*newSecret).Namespace) return retry.RetryOnConflict(retry.DefaultBackoff, func() error { currentSecret, err := secrets.Get(ctx, secretName, metav1.GetOptions{}) isNotFound := k8serrors.IsNotFound(err) diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go b/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go index e3ae8905..04ffbeda 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go @@ -21,10 +21,163 @@ import ( kubernetesfake "k8s.io/client-go/kubernetes/fake" kubetesting "k8s.io/client-go/testing" + configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1" "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/testutil" ) -func TestController(t *testing.T) { +var ( + owner = &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-owner-name", + UID: "some-owner-uid", + }, + } + + ownerGVK = schema.GroupVersionKind{ + Group: appsv1.SchemeGroupVersion.Group, + Version: appsv1.SchemeGroupVersion.Version, + Kind: "Deployment", + } + + labels = map[string]string{ + "some-label-key-1": "some-label-value-1", + "some-label-key-2": "some-label-value-2", + } +) + +func TestSupervisorSecretsControllerFilterSecret(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + secret corev1.Secret + wantAdd bool + wantUpdate bool + wantDelete bool + }{ + { + name: "no owner reference", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{}, + }, + }, + { + name: "owner reference without controller set to true", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: configv1alpha1.SchemeGroupVersion.String(), + Name: "some-name", + Kind: ownerGVK.Kind, + UID: owner.GetUID(), + }, + }, + }, + }, + }, + { + name: "owner reference without correct APIVersion", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "some-name", + Kind: ownerGVK.Kind, + Controller: boolPtr(true), + UID: owner.GetUID(), + }}, + }, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "owner reference without correct Kind", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: configv1alpha1.SchemeGroupVersion.String(), + Name: "some-name", + Kind: "IncorrectKind", + Controller: boolPtr(true), + UID: owner.GetUID(), + }, + }, + }, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "correct owner reference", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(owner, ownerGVK), + }, + }, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "multiple owner references", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "UnrelatedKind", + }, + *metav1.NewControllerRef(owner, ownerGVK), + }, + }, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + secretInformer := kubeinformers.NewSharedInformerFactory( + kubernetesfake.NewSimpleClientset(), + 0, + ).Core().V1().Secrets() + withInformer := testutil.NewObservableWithInformerOption() + _ = NewSupervisorSecretsController( + owner, + labels, + nil, // kubeClient, not needed + secretInformer, + nil, // setCache, not needed + withInformer.WithInformer, + ) + + unrelated := corev1.Secret{} + filter := withInformer.GetFilterForInformer(secretInformer) + require.Equal(t, test.wantAdd, filter.Add(&test.secret)) + require.Equal(t, test.wantUpdate, filter.Update(&unrelated, &test.secret)) + require.Equal(t, test.wantUpdate, filter.Update(&test.secret, &unrelated)) + require.Equal(t, test.wantDelete, filter.Delete(&test.secret)) + }) + } +} + +func TestSupervisorSecretsControllerSync(t *testing.T) { const ( generatedSecretNamespace = "some-namespace" generatedSecretName = "some-name-abc123" @@ -37,26 +190,9 @@ func TestController(t *testing.T) { Resource: "secrets", } - owner = &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "some-owner-name", - UID: "some-owner-uid", - }, - } - ownerGVK = schema.GroupVersionKind{ - Group: appsv1.SchemeGroupVersion.Group, - Version: appsv1.SchemeGroupVersion.Version, - Kind: "Deployment", - } - generatedSymmetricKey = []byte("some-neato-32-byte-generated-key") otherGeneratedSymmetricKey = []byte("some-funio-32-byte-generated-key") - labels = map[string]string{ - "some-label-key-1": "some-label-value-1", - "some-label-key-2": "some-label-value-2", - } - generatedSecret = &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: generatedSecretName, @@ -313,10 +449,17 @@ func TestController(t *testing.T) { secrets := informers.Core().V1().Secrets() var callbackSecret []byte - c := NewSupervisorSecretsController(owner, labels, apiClient, secrets, func(secret []byte) { - require.Nil(t, callbackSecret, "callback was called twice") - callbackSecret = secret - }) + c := NewSupervisorSecretsController( + owner, + labels, + apiClient, + secrets, + func(secret []byte) { + require.Nil(t, callbackSecret, "callback was called twice") + callbackSecret = secret + }, + testutil.NewObservableWithInformerOption().WithInformer, + ) // Must start informers before calling TestRunSynchronously(). informers.Start(ctx.Done()) From b1ee434ddfbd0de5d7af29e92ed9a880c4965c62 Mon Sep 17 00:00:00 2001 From: aram price Date: Mon, 14 Dec 2020 16:23:17 -0800 Subject: [PATCH 27/51] Rename in preparation for refactor --- cmd/pinniped-supervisor/main.go | 6 +-- .../symmetric_secret_helper.go | 38 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index 6c87b43e..53eec937 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -171,7 +171,7 @@ func startControllers( rand.Reader, func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { plog.Debug("setting hmac secret", "issuer", parent.Spec.Issuer) - secretCache.SetTokenHMACKey(parent.Spec.Issuer, child.Data[symmetricsecrethelper.SecretDataKey]) + secretCache.SetTokenHMACKey(parent.Spec.Issuer, child.Data[symmetricsecrethelper.SymmetricSecretDataKey]) }, ), kubeClient, @@ -189,7 +189,7 @@ func startControllers( rand.Reader, func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { plog.Debug("setting state signature key", "issuer", parent.Spec.Issuer) - secretCache.SetStateEncoderHashKey(parent.Spec.Issuer, child.Data[symmetricsecrethelper.SecretDataKey]) + secretCache.SetStateEncoderHashKey(parent.Spec.Issuer, child.Data[symmetricsecrethelper.SymmetricSecretDataKey]) }, ), kubeClient, @@ -207,7 +207,7 @@ func startControllers( rand.Reader, func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { plog.Debug("setting state encryption key", "issuer", parent.Spec.Issuer) - secretCache.SetStateEncoderBlockKey(parent.Spec.Issuer, child.Data[symmetricsecrethelper.SecretDataKey]) + secretCache.SetStateEncoderBlockKey(parent.Spec.Issuer, child.Data[symmetricsecrethelper.SymmetricSecretDataKey]) }, ), kubeClient, diff --git a/internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper.go b/internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper.go index f7baecf3..1d5c790c 100644 --- a/internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper.go +++ b/internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper.go @@ -18,25 +18,25 @@ import ( ) const ( - // SecretType is corev1.Secret.Type of all corev1.Secret's generated by this helper. - SecretType = "secrets.pinniped.dev/symmetric" - // SecretDataKey is the corev1.Secret.Data key for the symmetric key value generated by this helper. - SecretDataKey = "key" + // SymmetricSecretType is corev1.Secret.Type of all corev1.Secret's generated by this helper. + SymmetricSecretType = "secrets.pinniped.dev/symmetric" + // SymmetricSecretDataKey is the corev1.Secret.Data key for the symmetric key value generated by this helper. + SymmetricSecretDataKey = "key" - // keySize is the default length, in bytes, of generated keys. It is set to 32 since this + // symmetricKeySize is the default length, in bytes, of generated keys. It is set to 32 since this // seems like reasonable entropy for our keys, and a 32-byte key will allow for AES-256 // to be used in our codecs (see dynamiccodec.Codec). - keySize = 32 + symmetricKeySize = 32 ) -type helper struct { +type secretHelper struct { namePrefix string labels map[string]string rand io.Reader notifyFunc func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) } -var _ generator.SecretHelper = &helper{} +var _ generator.SecretHelper = &secretHelper{} // New returns a SecretHelper that has been parameterized with common symmetric secret generation // knobs. @@ -46,7 +46,7 @@ func New( rand io.Reader, notifyFunc func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret), ) generator.SecretHelper { - return &helper{ + return &secretHelper{ namePrefix: namePrefix, labels: labels, rand: rand, @@ -54,11 +54,11 @@ func New( } } -func (s *helper) Name() string { return s.namePrefix } +func (s *secretHelper) Name() string { return s.namePrefix } // Generate implements SecretHelper.Generate(). -func (s *helper) Generate(parent *configv1alpha1.OIDCProvider) (*corev1.Secret, error) { - key := make([]byte, keySize) +func (s *secretHelper) Generate(parent *configv1alpha1.OIDCProvider) (*corev1.Secret, error) { + key := make([]byte, symmetricKeySize) if _, err := s.rand.Read(key); err != nil { return nil, err } @@ -76,28 +76,28 @@ func (s *helper) Generate(parent *configv1alpha1.OIDCProvider) (*corev1.Secret, }), }, }, - Type: SecretType, + Type: SymmetricSecretType, Data: map[string][]byte{ - SecretDataKey: key, + SymmetricSecretDataKey: key, }, }, nil } // IsValid implements SecretHelper.IsValid(). -func (s *helper) IsValid(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) bool { +func (s *secretHelper) IsValid(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) bool { if !metav1.IsControlledBy(child, parent) { return false } - if child.Type != SecretType { + if child.Type != SymmetricSecretType { return false } - key, ok := child.Data[SecretDataKey] + key, ok := child.Data[SymmetricSecretDataKey] if !ok { return false } - if len(key) != keySize { + if len(key) != symmetricKeySize { return false } @@ -105,6 +105,6 @@ func (s *helper) IsValid(parent *configv1alpha1.OIDCProvider, child *corev1.Secr } // Notify implements SecretHelper.Notify(). -func (s *helper) Notify(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { +func (s *secretHelper) Notify(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { s.notifyFunc(parent, child) } From b799515f849469965a8caeadb641ed949c775473 Mon Sep 17 00:00:00 2001 From: aram price Date: Mon, 14 Dec 2020 17:38:01 -0800 Subject: [PATCH 28/51] Pull symmetricsecrethelper package up to generator - rename symmetricsecrethelper.New => generator.NewSymmetricSecretHelper --- cmd/pinniped-supervisor/main.go | 13 +++--- .../supervisorconfig/generator/generator.go | 13 ++---- .../generator/oidc_provider_secrets.go | 11 ----- ...tric_secret_helper.go => secret_helper.go} | 46 +++++++++++-------- ...t_helper_test.go => secret_helper_test.go} | 10 ++-- .../generator/supervisor_secrets.go | 4 +- 6 files changed, 43 insertions(+), 54 deletions(-) rename internal/controller/supervisorconfig/generator/{symmetricsecrethelper/symmetric_secret_helper.go => secret_helper.go} (71%) rename internal/controller/supervisorconfig/generator/{symmetricsecrethelper/symmetric_secret_helper_test.go => secret_helper_test.go} (91%) diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index 53eec937..bc142a65 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -36,7 +36,6 @@ import ( "go.pinniped.dev/internal/config/supervisor" "go.pinniped.dev/internal/controller/supervisorconfig" "go.pinniped.dev/internal/controller/supervisorconfig/generator" - "go.pinniped.dev/internal/controller/supervisorconfig/generator/symmetricsecrethelper" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatcher" "go.pinniped.dev/internal/controller/supervisorstorage" "go.pinniped.dev/internal/controllerlib" @@ -165,13 +164,13 @@ func startControllers( ). WithController( generator.NewOIDCProviderSecretsController( - symmetricsecrethelper.New( + generator.NewSymmetricSecretHelper( "pinniped-oidc-provider-hmac-key-", cfg.Labels, rand.Reader, func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { plog.Debug("setting hmac secret", "issuer", parent.Spec.Issuer) - secretCache.SetTokenHMACKey(parent.Spec.Issuer, child.Data[symmetricsecrethelper.SymmetricSecretDataKey]) + secretCache.SetTokenHMACKey(parent.Spec.Issuer, child.Data[generator.SymmetricSecretDataKey]) }, ), kubeClient, @@ -183,13 +182,13 @@ func startControllers( ). WithController( generator.NewOIDCProviderSecretsController( - symmetricsecrethelper.New( + generator.NewSymmetricSecretHelper( "pinniped-oidc-provider-upstream-state-signature-key-", cfg.Labels, rand.Reader, func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { plog.Debug("setting state signature key", "issuer", parent.Spec.Issuer) - secretCache.SetStateEncoderHashKey(parent.Spec.Issuer, child.Data[symmetricsecrethelper.SymmetricSecretDataKey]) + secretCache.SetStateEncoderHashKey(parent.Spec.Issuer, child.Data[generator.SymmetricSecretDataKey]) }, ), kubeClient, @@ -201,13 +200,13 @@ func startControllers( ). WithController( generator.NewOIDCProviderSecretsController( - symmetricsecrethelper.New( + generator.NewSymmetricSecretHelper( "pinniped-oidc-provider-upstream-state-encryption-key-", cfg.Labels, rand.Reader, func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { plog.Debug("setting state encryption key", "issuer", parent.Spec.Issuer) - secretCache.SetStateEncoderBlockKey(parent.Spec.Issuer, child.Data[symmetricsecrethelper.SymmetricSecretDataKey]) + secretCache.SetStateEncoderBlockKey(parent.Spec.Issuer, child.Data[generator.SymmetricSecretDataKey]) }, ), kubeClient, diff --git a/internal/controller/supervisorconfig/generator/generator.go b/internal/controller/supervisorconfig/generator/generator.go index bed18e36..3aeed955 100644 --- a/internal/controller/supervisorconfig/generator/generator.go +++ b/internal/controller/supervisorconfig/generator/generator.go @@ -15,11 +15,6 @@ import ( ) const ( - symmetricKeySecretType = "secrets.pinniped.dev/symmetric" - symmetricKeySecretDataKey = "key" - - symmetricKeySize = 32 - opKind = "OIDCProvider" ) @@ -32,11 +27,11 @@ func generateSymmetricKey() ([]byte, error) { } func isValid(secret *corev1.Secret) bool { - if secret.Type != symmetricKeySecretType { + if secret.Type != SymmetricSecretType { return false } - data, ok := secret.Data[symmetricKeySecretDataKey] + data, ok := secret.Data[SymmetricSecretDataKey] if !ok { return false } @@ -54,7 +49,7 @@ func secretDataFunc() (map[string][]byte, error) { } return map[string][]byte{ - symmetricKeySecretDataKey: symmetricKey, + SymmetricSecretDataKey: symmetricKey, }, nil } @@ -78,7 +73,7 @@ func generateSecret(namespace, name string, labels map[string]string, secretData }, Labels: labels, }, - Type: symmetricKeySecretType, + Type: SymmetricSecretType, Data: secretData, }, nil } diff --git a/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go index 4069e200..4baf0471 100644 --- a/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go +++ b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go @@ -22,17 +22,6 @@ import ( "go.pinniped.dev/internal/plog" ) -// SecretHelper describes an object that can Generate() a Secret and determine whether a Secret -// IsValid(). It can also be Notify()'d about a Secret being persisted. -// -// A SecretHelper has a Name() that can be used to identify it from other SecretHelper instances. -type SecretHelper interface { - Name() string - Generate(*configv1alpha1.OIDCProvider) (*corev1.Secret, error) - IsValid(*configv1alpha1.OIDCProvider, *corev1.Secret) bool - Notify(*configv1alpha1.OIDCProvider, *corev1.Secret) -} - type oidcProviderSecretsController struct { secretHelper SecretHelper kubeClient kubernetes.Interface diff --git a/internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper.go b/internal/controller/supervisorconfig/generator/secret_helper.go similarity index 71% rename from internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper.go rename to internal/controller/supervisorconfig/generator/secret_helper.go index 1d5c790c..76e136f1 100644 --- a/internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper.go +++ b/internal/controller/supervisorconfig/generator/secret_helper.go @@ -1,9 +1,7 @@ // Copyright 2020 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// Package symmetricsecrethelper provides a type that can generate and validate symmetric keys as -// Secret's. -package symmetricsecrethelper +package generator import ( "fmt" @@ -14,9 +12,19 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1" - "go.pinniped.dev/internal/controller/supervisorconfig/generator" ) +// SecretHelper describes an object that can Generate() a Secret and determine whether a Secret +// IsValid(). It can also be Notify()'d about a Secret being persisted. +// +// A SecretHelper has a Name() that can be used to identify it from other SecretHelper instances. +type SecretHelper interface { + Name() string + Generate(*configv1alpha1.OIDCProvider) (*corev1.Secret, error) + IsValid(*configv1alpha1.OIDCProvider, *corev1.Secret) bool + Notify(*configv1alpha1.OIDCProvider, *corev1.Secret) +} + const ( // SymmetricSecretType is corev1.Secret.Type of all corev1.Secret's generated by this helper. SymmetricSecretType = "secrets.pinniped.dev/symmetric" @@ -29,24 +37,15 @@ const ( symmetricKeySize = 32 ) -type secretHelper struct { - namePrefix string - labels map[string]string - rand io.Reader - notifyFunc func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) -} - -var _ generator.SecretHelper = &secretHelper{} - // New returns a SecretHelper that has been parameterized with common symmetric secret generation // knobs. -func New( +func NewSymmetricSecretHelper( namePrefix string, labels map[string]string, rand io.Reader, notifyFunc func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret), -) generator.SecretHelper { - return &secretHelper{ +) SecretHelper { + return &symmetricSecretHelper{ namePrefix: namePrefix, labels: labels, rand: rand, @@ -54,10 +53,17 @@ func New( } } -func (s *secretHelper) Name() string { return s.namePrefix } +type symmetricSecretHelper struct { + namePrefix string + labels map[string]string + rand io.Reader + notifyFunc func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) +} + +func (s *symmetricSecretHelper) Name() string { return s.namePrefix } // Generate implements SecretHelper.Generate(). -func (s *secretHelper) Generate(parent *configv1alpha1.OIDCProvider) (*corev1.Secret, error) { +func (s *symmetricSecretHelper) Generate(parent *configv1alpha1.OIDCProvider) (*corev1.Secret, error) { key := make([]byte, symmetricKeySize) if _, err := s.rand.Read(key); err != nil { return nil, err @@ -84,7 +90,7 @@ func (s *secretHelper) Generate(parent *configv1alpha1.OIDCProvider) (*corev1.Se } // IsValid implements SecretHelper.IsValid(). -func (s *secretHelper) IsValid(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) bool { +func (s *symmetricSecretHelper) IsValid(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) bool { if !metav1.IsControlledBy(child, parent) { return false } @@ -105,6 +111,6 @@ func (s *secretHelper) IsValid(parent *configv1alpha1.OIDCProvider, child *corev } // Notify implements SecretHelper.Notify(). -func (s *secretHelper) Notify(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { +func (s *symmetricSecretHelper) Notify(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { s.notifyFunc(parent, child) } diff --git a/internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper_test.go b/internal/controller/supervisorconfig/generator/secret_helper_test.go similarity index 91% rename from internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper_test.go rename to internal/controller/supervisorconfig/generator/secret_helper_test.go index b0787a0d..b0330626 100644 --- a/internal/controller/supervisorconfig/generator/symmetricsecrethelper/symmetric_secret_helper_test.go +++ b/internal/controller/supervisorconfig/generator/secret_helper_test.go @@ -1,7 +1,7 @@ // Copyright 2020 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package symmetricsecrethelper +package generator import ( "strings" @@ -17,7 +17,7 @@ import ( const keyWith32Bytes = "0123456789abcdef0123456789abcdef" -func TestHelper(t *testing.T) { +func TestSymmetricSecretHHelper(t *testing.T) { labels := map[string]string{ "some-label-key-1": "some-label-value-1", "some-label-key-2": "some-label-value-2", @@ -25,7 +25,7 @@ func TestHelper(t *testing.T) { randSource := strings.NewReader(keyWith32Bytes) var notifyParent *configv1alpha1.OIDCProvider var notifyChild *corev1.Secret - h := New("some-name-prefix-", labels, randSource, func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { + h := NewSymmetricSecretHelper("some-name-prefix-", labels, randSource, func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { require.True(t, notifyParent == nil && notifyChild == nil, "expected notify func not to have been called yet") notifyParent = parent notifyChild = child @@ -65,7 +65,7 @@ func TestHelper(t *testing.T) { require.Equal(t, child, notifyChild) } -func TestHelperIsValid(t *testing.T) { +func TestSymmetricSecretHHelperIsValid(t *testing.T) { tests := []struct { name string child func(*corev1.Secret) @@ -115,7 +115,7 @@ func TestHelperIsValid(t *testing.T) { for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - h := New("none of these args matter", nil, nil, nil) + h := NewSymmetricSecretHelper("none of these args matter", nil, nil, nil) parent := &configv1alpha1.OIDCProvider{ ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets.go b/internal/controller/supervisorconfig/generator/supervisor_secrets.go index e27166ae..58d5583d 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets.go @@ -78,7 +78,7 @@ func (c *supervisorSecretsController) Sync(ctx controllerlib.Context) error { secretNeedsUpdate := isNotFound || !isValid(secret) if !secretNeedsUpdate { plog.Debug("secret is up to date", "secret", klog.KObj(secret)) - c.setCacheFunc(secret.Data[symmetricKeySecretDataKey]) + c.setCacheFunc(secret.Data[SymmetricSecretDataKey]) return nil } @@ -96,7 +96,7 @@ func (c *supervisorSecretsController) Sync(ctx controllerlib.Context) error { return fmt.Errorf("failed to create/update secret %s/%s: %w", newSecret.Namespace, newSecret.Name, err) } - c.setCacheFunc(newSecret.Data[symmetricKeySecretDataKey]) + c.setCacheFunc(newSecret.Data[SymmetricSecretDataKey]) return nil } From bf86bc3383c3617bf532aed952b478a908fe36c3 Mon Sep 17 00:00:00 2001 From: aram price Date: Mon, 14 Dec 2020 18:36:56 -0800 Subject: [PATCH 29/51] Rename for clarity --- .../generator/oidc_provider_secrets.go | 2 +- .../generator/oidc_provider_secrets_test.go | 6 +++--- .../supervisorconfig/generator/secret_helper.go | 6 +++--- internal/mocks/mocksecrethelper/mocksecrethelper.go | 12 ++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go index 4baf0471..01f0c491 100644 --- a/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go +++ b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go @@ -41,7 +41,7 @@ func NewOIDCProviderSecretsController( ) controllerlib.Controller { return controllerlib.New( controllerlib.Config{ - Name: fmt.Sprintf("%s%s", secretHelper.Name(), "controller"), + Name: fmt.Sprintf("%s%s", secretHelper.NamePrefix(), "controller"), Syncer: &oidcProviderSecretsController{ secretHelper: secretHelper, kubeClient: kubeClient, diff --git a/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go b/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go index a271aa22..68cbc4bf 100644 --- a/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go +++ b/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go @@ -144,7 +144,7 @@ func TestOIDCProviderControllerFilterSecret(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) secretHelper := mocksecrethelper.NewMockSecretHelper(ctrl) - secretHelper.EXPECT().Name().Times(1).Return("some-name") + secretHelper.EXPECT().NamePrefix().Times(1).Return("some-name") secretInformer := kubeinformers.NewSharedInformerFactory( kubernetesfake.NewSimpleClientset(), @@ -202,7 +202,7 @@ func TestNewOIDCProviderSecretsControllerFilterOPC(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) secretHelper := mocksecrethelper.NewMockSecretHelper(ctrl) - secretHelper.EXPECT().Name().Times(1).Return("some-name") + secretHelper.EXPECT().NamePrefix().Times(1).Return("some-name") secretInformer := kubeinformers.NewSharedInformerFactory( kubernetesfake.NewSimpleClientset(), @@ -511,7 +511,7 @@ func TestOIDCProviderSecretsControllerSync(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) secretHelper := mocksecrethelper.NewMockSecretHelper(ctrl) - secretHelper.EXPECT().Name().Times(1).Return("some-name") + secretHelper.EXPECT().NamePrefix().Times(1).Return("some-name") if test.secretHelper != nil { test.secretHelper(secretHelper) } diff --git a/internal/controller/supervisorconfig/generator/secret_helper.go b/internal/controller/supervisorconfig/generator/secret_helper.go index 76e136f1..54ba49f5 100644 --- a/internal/controller/supervisorconfig/generator/secret_helper.go +++ b/internal/controller/supervisorconfig/generator/secret_helper.go @@ -17,9 +17,9 @@ import ( // SecretHelper describes an object that can Generate() a Secret and determine whether a Secret // IsValid(). It can also be Notify()'d about a Secret being persisted. // -// A SecretHelper has a Name() that can be used to identify it from other SecretHelper instances. +// A SecretHelper has a NamePrefix() that can be used to identify it from other SecretHelper instances. type SecretHelper interface { - Name() string + NamePrefix() string Generate(*configv1alpha1.OIDCProvider) (*corev1.Secret, error) IsValid(*configv1alpha1.OIDCProvider, *corev1.Secret) bool Notify(*configv1alpha1.OIDCProvider, *corev1.Secret) @@ -60,7 +60,7 @@ type symmetricSecretHelper struct { notifyFunc func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) } -func (s *symmetricSecretHelper) Name() string { return s.namePrefix } +func (s *symmetricSecretHelper) NamePrefix() string { return s.namePrefix } // Generate implements SecretHelper.Generate(). func (s *symmetricSecretHelper) Generate(parent *configv1alpha1.OIDCProvider) (*corev1.Secret, error) { diff --git a/internal/mocks/mocksecrethelper/mocksecrethelper.go b/internal/mocks/mocksecrethelper/mocksecrethelper.go index 68b185f3..d191d2d9 100644 --- a/internal/mocks/mocksecrethelper/mocksecrethelper.go +++ b/internal/mocks/mocksecrethelper/mocksecrethelper.go @@ -67,18 +67,18 @@ func (mr *MockSecretHelperMockRecorder) IsValid(arg0, arg1 interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsValid", reflect.TypeOf((*MockSecretHelper)(nil).IsValid), arg0, arg1) } -// Name mocks base method -func (m *MockSecretHelper) Name() string { +// NamePrefix mocks base method +func (m *MockSecretHelper) NamePrefix() string { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Name") + ret := m.ctrl.Call(m, "NamePrefix") ret0, _ := ret[0].(string) return ret0 } -// Name indicates an expected call of Name -func (mr *MockSecretHelperMockRecorder) Name() *gomock.Call { +// NamePrefix indicates an expected call of NamePrefix +func (mr *MockSecretHelperMockRecorder) NamePrefix() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockSecretHelper)(nil).Name)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NamePrefix", reflect.TypeOf((*MockSecretHelper)(nil).NamePrefix)) } // Notify mocks base method From e03e344dcd617a56c96d2b48a237b4f30a93c6cd Mon Sep 17 00:00:00 2001 From: aram price Date: Mon, 14 Dec 2020 19:35:45 -0800 Subject: [PATCH 30/51] SecretHelper depends less on OIDCProvider This should allow the helper to be more generic so that it can be used with the SupervisorSecretsController --- cmd/pinniped-supervisor/main.go | 20 +++++------ .../generator/secret_helper.go | 35 +++++++++++-------- .../generator/secret_helper_test.go | 18 +++++----- 3 files changed, 39 insertions(+), 34 deletions(-) diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index bc142a65..d973ed27 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -18,7 +18,6 @@ import ( "go.pinniped.dev/internal/secret" appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/clock" kubeinformers "k8s.io/client-go/informers" @@ -30,7 +29,6 @@ import ( "k8s.io/klog/v2" "k8s.io/klog/v2/klogr" - configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1" pinnipedclientset "go.pinniped.dev/generated/1.19/client/supervisor/clientset/versioned" pinnipedinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/config/supervisor" @@ -168,9 +166,9 @@ func startControllers( "pinniped-oidc-provider-hmac-key-", cfg.Labels, rand.Reader, - func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { - plog.Debug("setting hmac secret", "issuer", parent.Spec.Issuer) - secretCache.SetTokenHMACKey(parent.Spec.Issuer, child.Data[generator.SymmetricSecretDataKey]) + func(oidcProviderIssuer string, symmetricKey []byte) { + plog.Debug("setting hmac secret", "issuer", oidcProviderIssuer) + secretCache.SetTokenHMACKey(oidcProviderIssuer, symmetricKey) }, ), kubeClient, @@ -186,9 +184,9 @@ func startControllers( "pinniped-oidc-provider-upstream-state-signature-key-", cfg.Labels, rand.Reader, - func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { - plog.Debug("setting state signature key", "issuer", parent.Spec.Issuer) - secretCache.SetStateEncoderHashKey(parent.Spec.Issuer, child.Data[generator.SymmetricSecretDataKey]) + func(oidcProviderIssuer string, symmetricKey []byte) { + plog.Debug("setting state signature key", "issuer", oidcProviderIssuer) + secretCache.SetStateEncoderHashKey(oidcProviderIssuer, symmetricKey) }, ), kubeClient, @@ -204,9 +202,9 @@ func startControllers( "pinniped-oidc-provider-upstream-state-encryption-key-", cfg.Labels, rand.Reader, - func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { - plog.Debug("setting state encryption key", "issuer", parent.Spec.Issuer) - secretCache.SetStateEncoderBlockKey(parent.Spec.Issuer, child.Data[generator.SymmetricSecretDataKey]) + func(oidcProviderIssuer string, symmetricKey []byte) { + plog.Debug("setting state encryption key", "issuer", oidcProviderIssuer) + secretCache.SetStateEncoderBlockKey(oidcProviderIssuer, symmetricKey) }, ), kubeClient, diff --git a/internal/controller/supervisorconfig/generator/secret_helper.go b/internal/controller/supervisorconfig/generator/secret_helper.go index 54ba49f5..cfbcb5a7 100644 --- a/internal/controller/supervisorconfig/generator/secret_helper.go +++ b/internal/controller/supervisorconfig/generator/secret_helper.go @@ -43,21 +43,21 @@ func NewSymmetricSecretHelper( namePrefix string, labels map[string]string, rand io.Reader, - notifyFunc func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret), + updateCacheFunc func(cacheKey string, cacheValue []byte), ) SecretHelper { return &symmetricSecretHelper{ - namePrefix: namePrefix, - labels: labels, - rand: rand, - notifyFunc: notifyFunc, + namePrefix: namePrefix, + labels: labels, + rand: rand, + updateCacheFunc: updateCacheFunc, } } type symmetricSecretHelper struct { - namePrefix string - labels map[string]string - rand io.Reader - notifyFunc func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) + namePrefix string + labels map[string]string + rand io.Reader + updateCacheFunc func(cacheKey string, cacheValue []byte) } func (s *symmetricSecretHelper) NamePrefix() string { return s.namePrefix } @@ -90,16 +90,16 @@ func (s *symmetricSecretHelper) Generate(parent *configv1alpha1.OIDCProvider) (* } // IsValid implements SecretHelper.IsValid(). -func (s *symmetricSecretHelper) IsValid(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) bool { - if !metav1.IsControlledBy(child, parent) { +func (s *symmetricSecretHelper) IsValid(parent *configv1alpha1.OIDCProvider, secret *corev1.Secret) bool { + if !metav1.IsControlledBy(secret, parent) { return false } - if child.Type != SymmetricSecretType { + if secret.Type != SymmetricSecretType { return false } - key, ok := child.Data[SymmetricSecretDataKey] + key, ok := secret.Data[SymmetricSecretDataKey] if !ok { return false } @@ -111,6 +111,11 @@ func (s *symmetricSecretHelper) IsValid(parent *configv1alpha1.OIDCProvider, chi } // Notify implements SecretHelper.Notify(). -func (s *symmetricSecretHelper) Notify(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { - s.notifyFunc(parent, child) +func (s *symmetricSecretHelper) Notify(op *configv1alpha1.OIDCProvider, secret *corev1.Secret) { + var cacheKey string + if op != nil { + cacheKey = op.Spec.Issuer + } + + s.updateCacheFunc(cacheKey, secret.Data[SymmetricSecretDataKey]) } diff --git a/internal/controller/supervisorconfig/generator/secret_helper_test.go b/internal/controller/supervisorconfig/generator/secret_helper_test.go index b0330626..ffe89241 100644 --- a/internal/controller/supervisorconfig/generator/secret_helper_test.go +++ b/internal/controller/supervisorconfig/generator/secret_helper_test.go @@ -23,12 +23,14 @@ func TestSymmetricSecretHHelper(t *testing.T) { "some-label-key-2": "some-label-value-2", } randSource := strings.NewReader(keyWith32Bytes) - var notifyParent *configv1alpha1.OIDCProvider - var notifyChild *corev1.Secret - h := NewSymmetricSecretHelper("some-name-prefix-", labels, randSource, func(parent *configv1alpha1.OIDCProvider, child *corev1.Secret) { - require.True(t, notifyParent == nil && notifyChild == nil, "expected notify func not to have been called yet") - notifyParent = parent - notifyChild = child + // var notifyParent *configv1alpha1.OIDCProvider + // var notifyChild *corev1.Secret + var oidcProviderIssuerValue string + var symmetricKeyValue []byte + h := NewSymmetricSecretHelper("some-name-prefix-", labels, randSource, func(oidcProviderIssuer string, symmetricKey []byte) { + require.True(t, oidcProviderIssuer == "" && symmetricKeyValue == nil, "expected notify func not to have been called yet") + oidcProviderIssuerValue = oidcProviderIssuer + symmetricKeyValue = symmetricKey }) parent := &configv1alpha1.OIDCProvider{ @@ -61,8 +63,8 @@ func TestSymmetricSecretHHelper(t *testing.T) { require.True(t, h.IsValid(parent, child)) h.Notify(parent, child) - require.Equal(t, parent, notifyParent) - require.Equal(t, child, notifyChild) + require.Equal(t, parent.Spec.Issuer, oidcProviderIssuerValue) + require.Equal(t, child.Data[SymmetricSecretDataKey], symmetricKeyValue) } func TestSymmetricSecretHHelperIsValid(t *testing.T) { From 9a3e60d4dffdeb14697ea6802ff4b7e3cac80383 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Tue, 15 Dec 2020 07:56:31 -0500 Subject: [PATCH 31/51] go.mod: unnecessary dependency slipped in (c3f73ff) Signed-off-by: Andrew Keesler --- go.mod | 1 - 1 file changed, 1 deletion(-) diff --git a/go.mod b/go.mod index 3abd0ba1..84237fd7 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,6 @@ require ( github.com/ory/fosite v0.35.1 github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/pkg/errors v0.9.1 - github.com/prometheus/common v0.10.0 github.com/sclevine/agouti v3.0.0+incompatible github.com/sclevine/spec v1.4.0 github.com/spf13/cobra v1.0.0 From 60d4a7beac5eb1d575c6d9ce6100663e26c3dc0e Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Tue, 15 Dec 2020 07:58:33 -0500 Subject: [PATCH 32/51] Test more filters in SupervisorSecretsController (see 6e8d5640135f) Signed-off-by: Andrew Keesler --- cmd/pinniped-supervisor/main.go | 1 + .../generator/supervisor_secrets.go | 3 +- .../generator/supervisor_secrets_test.go | 33 ++++++++++++++++--- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index d973ed27..07cec6f2 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -157,6 +157,7 @@ func startControllers( secretCache.SetCSRFCookieEncoderHashKey(secret) }, controllerlib.WithInformer, + controllerlib.WithInitialEvent, ), singletonWorker, ). diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets.go b/internal/controller/supervisorconfig/generator/supervisor_secrets.go index 58d5583d..ec89993f 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets.go @@ -43,6 +43,7 @@ func NewSupervisorSecretsController( secretInformer corev1informers.SecretInformer, setCacheFunc func(secret []byte), withInformer pinnipedcontroller.WithInformerOptionFunc, + initialEventFunc pinnipedcontroller.WithInitialEventOptionFunc, ) controllerlib.Controller { c := supervisorSecretsController{ owner: owner, @@ -60,7 +61,7 @@ func NewSupervisorSecretsController( }, nil), controllerlib.InformerOption{}, ), - controllerlib.WithInitialEvent(controllerlib.Key{ + initialEventFunc(controllerlib.Key{ Namespace: owner.Namespace, Name: owner.Name + "-key", }), diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go b/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go index 04ffbeda..c5357ce1 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go @@ -21,7 +21,6 @@ import ( kubernetesfake "k8s.io/client-go/kubernetes/fake" kubetesting "k8s.io/client-go/testing" - configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/testutil" ) @@ -29,8 +28,9 @@ import ( var ( owner = &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: "some-owner-name", - UID: "some-owner-uid", + Name: "some-owner-name", + Namespace: "some-namespace", + UID: "some-owner-uid", }, } @@ -69,7 +69,7 @@ func TestSupervisorSecretsControllerFilterSecret(t *testing.T) { Namespace: "some-namespace", OwnerReferences: []metav1.OwnerReference{ { - APIVersion: configv1alpha1.SchemeGroupVersion.String(), + APIVersion: ownerGVK.String(), Name: "some-name", Kind: ownerGVK.Kind, UID: owner.GetUID(), @@ -103,7 +103,7 @@ func TestSupervisorSecretsControllerFilterSecret(t *testing.T) { Namespace: "some-namespace", OwnerReferences: []metav1.OwnerReference{ { - APIVersion: configv1alpha1.SchemeGroupVersion.String(), + APIVersion: ownerGVK.String(), Name: "some-name", Kind: "IncorrectKind", Controller: boolPtr(true), @@ -165,6 +165,7 @@ func TestSupervisorSecretsControllerFilterSecret(t *testing.T) { secretInformer, nil, // setCache, not needed withInformer.WithInformer, + testutil.NewObservableWithInitialEventOption().WithInitialEvent, ) unrelated := corev1.Secret{} @@ -177,6 +178,27 @@ func TestSupervisorSecretsControllerFilterSecret(t *testing.T) { } } +func TestSupervisorSecretsControllerInitialEvent(t *testing.T) { + initialEventOption := testutil.NewObservableWithInitialEventOption() + secretInformer := kubeinformers.NewSharedInformerFactory( + kubernetesfake.NewSimpleClientset(), + 0, + ).Core().V1().Secrets() + _ = NewSupervisorSecretsController( + owner, + nil, + nil, // kubeClient, not needed + secretInformer, + nil, // setCache, not needed + testutil.NewObservableWithInformerOption().WithInformer, + initialEventOption.WithInitialEvent, + ) + require.Equal(t, &controllerlib.Key{ + owner.Namespace, + owner.Name + "-key", + }, initialEventOption.GetInitialEventKey()) +} + func TestSupervisorSecretsControllerSync(t *testing.T) { const ( generatedSecretNamespace = "some-namespace" @@ -459,6 +481,7 @@ func TestSupervisorSecretsControllerSync(t *testing.T) { callbackSecret = secret }, testutil.NewObservableWithInformerOption().WithInformer, + testutil.NewObservableWithInitialEventOption().WithInitialEvent, ) // Must start informers before calling TestRunSynchronously(). From 82ae98d9d0653ca0b73ec94895ce748c1988674b Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Tue, 15 Dec 2020 09:13:01 -0500 Subject: [PATCH 33/51] Set secret names on OIDCProvider status field We believe this API is more forwards compatible with future secrets management use cases. The implementation is a cry for help, but I was trying to follow the previously established pattern of encapsulating the secret generation functionality to a single group of packages. This commit makes a breaking change to the current OIDCProvider API, but that OIDCProvider API was added after the latest release, so it is technically still in development until we release, and therefore we can continue to thrash on it. I also took this opportunity to make some things private that didn't need to be public. Signed-off-by: Andrew Keesler --- .../v1alpha1/types_oidcprovider.go.tmpl | 30 +++- cmd/pinniped-supervisor/main.go | 6 + ...supervisor.pinniped.dev_oidcproviders.yaml | 56 ++++++-- generated/1.17/README.adoc | 22 ++- .../config/v1alpha1/types_oidcprovider.go | 30 +++- .../config/v1alpha1/zz_generated.deepcopy.go | 22 ++- ...supervisor.pinniped.dev_oidcproviders.yaml | 56 ++++++-- generated/1.18/README.adoc | 22 ++- .../config/v1alpha1/types_oidcprovider.go | 30 +++- .../config/v1alpha1/zz_generated.deepcopy.go | 22 ++- ...supervisor.pinniped.dev_oidcproviders.yaml | 56 ++++++-- generated/1.19/README.adoc | 22 ++- .../config/v1alpha1/types_oidcprovider.go | 30 +++- .../config/v1alpha1/zz_generated.deepcopy.go | 22 ++- ...supervisor.pinniped.dev_oidcproviders.yaml | 56 ++++++-- .../supervisorconfig/generator/generator.go | 8 +- .../generator/oidc_provider_secrets.go | 40 +++++- .../generator/oidc_provider_secrets_test.go | 126 +++++++++++++---- .../generator/secret_helper.go | 54 ++++++-- .../generator/secret_helper_test.go | 129 ++++++++++++------ .../generator/supervisor_secrets.go | 4 +- .../generator/supervisor_secrets_test.go | 4 +- .../supervisorconfig/jwks_observer.go | 2 +- .../supervisorconfig/jwks_observer_test.go | 22 ++- .../supervisorconfig/jwks_writer.go | 10 +- .../supervisorconfig/jwks_writer_test.go | 4 +- .../mocksecrethelper/mocksecrethelper.go | 14 +- test/integration/supervisor_secrets_test.go | 8 +- 28 files changed, 724 insertions(+), 183 deletions(-) diff --git a/apis/supervisor/config/v1alpha1/types_oidcprovider.go.tmpl b/apis/supervisor/config/v1alpha1/types_oidcprovider.go.tmpl index 908470f0..2e31219e 100644 --- a/apis/supervisor/config/v1alpha1/types_oidcprovider.go.tmpl +++ b/apis/supervisor/config/v1alpha1/types_oidcprovider.go.tmpl @@ -59,6 +59,30 @@ type OIDCProviderSpec struct { TLS *OIDCProviderTLSSpec `json:"tls,omitempty"` } +// OIDCProviderSecrets holds information about this OIDC Provider's secrets. +type OIDCProviderSecrets struct { + // JWKS holds the name of the corev1.Secret in which this OIDC Provider's signing/verification keys are + // stored. If it is empty, then the signing/verification keys are either unknown or they don't + // exist. + // +optional + JWKS corev1.LocalObjectReference `json:"jwks,omitempty"` + + // TokenSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for + // signing tokens is stored. + // +optional + TokenSigningKey corev1.LocalObjectReference `json:"tokenSigningKey,omitempty"` + + // StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for + // signing state parameters is stored. + // +optional + StateSigningKey corev1.LocalObjectReference `json:"stateSigningKey,omitempty"` + + // StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for + // encrypting state parameters is stored. + // +optional + StateEncryptionKey corev1.LocalObjectReference `json:"stateEncryptionKey,omitempty"` +} + // OIDCProviderStatus is a struct that describes the actual state of an OIDC Provider. type OIDCProviderStatus struct { // Status holds an enum that describes the state of this OIDC Provider. Note that this Status can @@ -76,11 +100,9 @@ type OIDCProviderStatus struct { // +optional LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` - // JWKSSecret holds the name of the secret in which this OIDC Provider's signing/verification keys - // are stored. If it is empty, then the signing/verification keys are either unknown or they don't - // exist. + // Secrets contains information about this OIDC Provider's secrets. // +optional - JWKSSecret corev1.LocalObjectReference `json:"jwksSecret,omitempty"` + Secrets OIDCProviderSecrets `json:"secrets,omitempty"` } // OIDCProvider describes the configuration of an OIDC provider. diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index 07cec6f2..461eea56 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -167,12 +167,14 @@ func startControllers( "pinniped-oidc-provider-hmac-key-", cfg.Labels, rand.Reader, + generator.SecretUsageTokenSigningKey, func(oidcProviderIssuer string, symmetricKey []byte) { plog.Debug("setting hmac secret", "issuer", oidcProviderIssuer) secretCache.SetTokenHMACKey(oidcProviderIssuer, symmetricKey) }, ), kubeClient, + pinnipedClient, secretInformer, opInformer, controllerlib.WithInformer, @@ -185,12 +187,14 @@ func startControllers( "pinniped-oidc-provider-upstream-state-signature-key-", cfg.Labels, rand.Reader, + generator.SecretUsageStateSigningKey, func(oidcProviderIssuer string, symmetricKey []byte) { plog.Debug("setting state signature key", "issuer", oidcProviderIssuer) secretCache.SetStateEncoderHashKey(oidcProviderIssuer, symmetricKey) }, ), kubeClient, + pinnipedClient, secretInformer, opInformer, controllerlib.WithInformer, @@ -203,12 +207,14 @@ func startControllers( "pinniped-oidc-provider-upstream-state-encryption-key-", cfg.Labels, rand.Reader, + generator.SecretUsageStateEncryptionKey, func(oidcProviderIssuer string, symmetricKey []byte) { plog.Debug("setting state encryption key", "issuer", oidcProviderIssuer) secretCache.SetStateEncoderBlockKey(oidcProviderIssuer, symmetricKey) }, ), kubeClient, + pinnipedClient, secretInformer, opInformer, controllerlib.WithInformer, diff --git a/deploy/supervisor/config.supervisor.pinniped.dev_oidcproviders.yaml b/deploy/supervisor/config.supervisor.pinniped.dev_oidcproviders.yaml index 6ff3a42b..18771821 100644 --- a/deploy/supervisor/config.supervisor.pinniped.dev_oidcproviders.yaml +++ b/deploy/supervisor/config.supervisor.pinniped.dev_oidcproviders.yaml @@ -81,17 +81,6 @@ spec: status: description: Status of the OIDC provider. properties: - jwksSecret: - description: JWKSSecret holds the name of the secret in which this - OIDC Provider's signing/verification keys are stored. If it is empty, - then the signing/verification keys are either unknown or they don't - exist. - properties: - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - type: object lastUpdateTime: description: LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior @@ -101,6 +90,51 @@ spec: message: description: Message provides human-readable details about the Status. type: string + secrets: + description: Secrets contains information about this OIDC Provider's + secrets. + properties: + jwks: + description: JWKS holds the name of the corev1.Secret in which + this OIDC Provider's signing/verification keys are stored. If + it is empty, then the signing/verification keys are either unknown + or they don't exist. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + stateEncryptionKey: + description: StateSigningKey holds the name of the corev1.Secret + in which this OIDC Provider's key for encrypting state parameters + is stored. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + stateSigningKey: + description: StateSigningKey holds the name of the corev1.Secret + in which this OIDC Provider's key for signing state parameters + is stored. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + tokenSigningKey: + description: TokenSigningKey holds the name of the corev1.Secret + in which this OIDC Provider's key for signing tokens is stored. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + type: object status: description: Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure. diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 0ccdd1df..5af68c1e 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -304,6 +304,26 @@ OIDCProvider describes the configuration of an OIDC provider. +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcprovidersecrets"] +==== OIDCProviderSecrets + +OIDCProviderSecrets holds information about this OIDC Provider's secrets. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcproviderstatus[$$OIDCProviderStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`jwks`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | JWKS holds the name of the corev1.Secret in which this OIDC Provider's signing/verification keys are stored. If it is empty, then the signing/verification keys are either unknown or they don't exist. +| *`tokenSigningKey`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | TokenSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for signing tokens is stored. +| *`stateSigningKey`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for signing state parameters is stored. +| *`stateEncryptionKey`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for encrypting state parameters is stored. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcproviderspec"] ==== OIDCProviderSpec @@ -339,7 +359,7 @@ OIDCProviderStatus is a struct that describes the actual state of an OIDC Provid | *`status`* __OIDCProviderStatusCondition__ | Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure. | *`message`* __string__ | Message provides human-readable details about the Status. | *`lastUpdateTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#time-v1-meta[$$Time$$]__ | LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). -| *`jwksSecret`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | JWKSSecret holds the name of the secret in which this OIDC Provider's signing/verification keys are stored. If it is empty, then the signing/verification keys are either unknown or they don't exist. +| *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcprovidersecrets[$$OIDCProviderSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== diff --git a/generated/1.17/apis/supervisor/config/v1alpha1/types_oidcprovider.go b/generated/1.17/apis/supervisor/config/v1alpha1/types_oidcprovider.go index 908470f0..2e31219e 100644 --- a/generated/1.17/apis/supervisor/config/v1alpha1/types_oidcprovider.go +++ b/generated/1.17/apis/supervisor/config/v1alpha1/types_oidcprovider.go @@ -59,6 +59,30 @@ type OIDCProviderSpec struct { TLS *OIDCProviderTLSSpec `json:"tls,omitempty"` } +// OIDCProviderSecrets holds information about this OIDC Provider's secrets. +type OIDCProviderSecrets struct { + // JWKS holds the name of the corev1.Secret in which this OIDC Provider's signing/verification keys are + // stored. If it is empty, then the signing/verification keys are either unknown or they don't + // exist. + // +optional + JWKS corev1.LocalObjectReference `json:"jwks,omitempty"` + + // TokenSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for + // signing tokens is stored. + // +optional + TokenSigningKey corev1.LocalObjectReference `json:"tokenSigningKey,omitempty"` + + // StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for + // signing state parameters is stored. + // +optional + StateSigningKey corev1.LocalObjectReference `json:"stateSigningKey,omitempty"` + + // StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for + // encrypting state parameters is stored. + // +optional + StateEncryptionKey corev1.LocalObjectReference `json:"stateEncryptionKey,omitempty"` +} + // OIDCProviderStatus is a struct that describes the actual state of an OIDC Provider. type OIDCProviderStatus struct { // Status holds an enum that describes the state of this OIDC Provider. Note that this Status can @@ -76,11 +100,9 @@ type OIDCProviderStatus struct { // +optional LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` - // JWKSSecret holds the name of the secret in which this OIDC Provider's signing/verification keys - // are stored. If it is empty, then the signing/verification keys are either unknown or they don't - // exist. + // Secrets contains information about this OIDC Provider's secrets. // +optional - JWKSSecret corev1.LocalObjectReference `json:"jwksSecret,omitempty"` + Secrets OIDCProviderSecrets `json:"secrets,omitempty"` } // OIDCProvider describes the configuration of an OIDC provider. diff --git a/generated/1.17/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.17/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index f208d4d0..0cfc17a4 100644 --- a/generated/1.17/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.17/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -72,6 +72,26 @@ func (in *OIDCProviderList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCProviderSecrets) DeepCopyInto(out *OIDCProviderSecrets) { + *out = *in + out.JWKS = in.JWKS + out.TokenSigningKey = in.TokenSigningKey + out.StateSigningKey = in.StateSigningKey + out.StateEncryptionKey = in.StateEncryptionKey + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCProviderSecrets. +func (in *OIDCProviderSecrets) DeepCopy() *OIDCProviderSecrets { + if in == nil { + return nil + } + out := new(OIDCProviderSecrets) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCProviderSpec) DeepCopyInto(out *OIDCProviderSpec) { *out = *in @@ -100,7 +120,7 @@ func (in *OIDCProviderStatus) DeepCopyInto(out *OIDCProviderStatus) { in, out := &in.LastUpdateTime, &out.LastUpdateTime *out = (*in).DeepCopy() } - out.JWKSSecret = in.JWKSSecret + out.Secrets = in.Secrets return } diff --git a/generated/1.17/crds/config.supervisor.pinniped.dev_oidcproviders.yaml b/generated/1.17/crds/config.supervisor.pinniped.dev_oidcproviders.yaml index 6ff3a42b..18771821 100644 --- a/generated/1.17/crds/config.supervisor.pinniped.dev_oidcproviders.yaml +++ b/generated/1.17/crds/config.supervisor.pinniped.dev_oidcproviders.yaml @@ -81,17 +81,6 @@ spec: status: description: Status of the OIDC provider. properties: - jwksSecret: - description: JWKSSecret holds the name of the secret in which this - OIDC Provider's signing/verification keys are stored. If it is empty, - then the signing/verification keys are either unknown or they don't - exist. - properties: - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - type: object lastUpdateTime: description: LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior @@ -101,6 +90,51 @@ spec: message: description: Message provides human-readable details about the Status. type: string + secrets: + description: Secrets contains information about this OIDC Provider's + secrets. + properties: + jwks: + description: JWKS holds the name of the corev1.Secret in which + this OIDC Provider's signing/verification keys are stored. If + it is empty, then the signing/verification keys are either unknown + or they don't exist. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + stateEncryptionKey: + description: StateSigningKey holds the name of the corev1.Secret + in which this OIDC Provider's key for encrypting state parameters + is stored. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + stateSigningKey: + description: StateSigningKey holds the name of the corev1.Secret + in which this OIDC Provider's key for signing state parameters + is stored. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + tokenSigningKey: + description: TokenSigningKey holds the name of the corev1.Secret + in which this OIDC Provider's key for signing tokens is stored. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + type: object status: description: Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure. diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index 97042b25..f0bca6a4 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -304,6 +304,26 @@ OIDCProvider describes the configuration of an OIDC provider. +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-config-v1alpha1-oidcprovidersecrets"] +==== OIDCProviderSecrets + +OIDCProviderSecrets holds information about this OIDC Provider's secrets. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-config-v1alpha1-oidcproviderstatus[$$OIDCProviderStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`jwks`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | JWKS holds the name of the corev1.Secret in which this OIDC Provider's signing/verification keys are stored. If it is empty, then the signing/verification keys are either unknown or they don't exist. +| *`tokenSigningKey`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | TokenSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for signing tokens is stored. +| *`stateSigningKey`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for signing state parameters is stored. +| *`stateEncryptionKey`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for encrypting state parameters is stored. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-config-v1alpha1-oidcproviderspec"] ==== OIDCProviderSpec @@ -339,7 +359,7 @@ OIDCProviderStatus is a struct that describes the actual state of an OIDC Provid | *`status`* __OIDCProviderStatusCondition__ | Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure. | *`message`* __string__ | Message provides human-readable details about the Status. | *`lastUpdateTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#time-v1-meta[$$Time$$]__ | LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). -| *`jwksSecret`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | JWKSSecret holds the name of the secret in which this OIDC Provider's signing/verification keys are stored. If it is empty, then the signing/verification keys are either unknown or they don't exist. +| *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-config-v1alpha1-oidcprovidersecrets[$$OIDCProviderSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== diff --git a/generated/1.18/apis/supervisor/config/v1alpha1/types_oidcprovider.go b/generated/1.18/apis/supervisor/config/v1alpha1/types_oidcprovider.go index 908470f0..2e31219e 100644 --- a/generated/1.18/apis/supervisor/config/v1alpha1/types_oidcprovider.go +++ b/generated/1.18/apis/supervisor/config/v1alpha1/types_oidcprovider.go @@ -59,6 +59,30 @@ type OIDCProviderSpec struct { TLS *OIDCProviderTLSSpec `json:"tls,omitempty"` } +// OIDCProviderSecrets holds information about this OIDC Provider's secrets. +type OIDCProviderSecrets struct { + // JWKS holds the name of the corev1.Secret in which this OIDC Provider's signing/verification keys are + // stored. If it is empty, then the signing/verification keys are either unknown or they don't + // exist. + // +optional + JWKS corev1.LocalObjectReference `json:"jwks,omitempty"` + + // TokenSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for + // signing tokens is stored. + // +optional + TokenSigningKey corev1.LocalObjectReference `json:"tokenSigningKey,omitempty"` + + // StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for + // signing state parameters is stored. + // +optional + StateSigningKey corev1.LocalObjectReference `json:"stateSigningKey,omitempty"` + + // StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for + // encrypting state parameters is stored. + // +optional + StateEncryptionKey corev1.LocalObjectReference `json:"stateEncryptionKey,omitempty"` +} + // OIDCProviderStatus is a struct that describes the actual state of an OIDC Provider. type OIDCProviderStatus struct { // Status holds an enum that describes the state of this OIDC Provider. Note that this Status can @@ -76,11 +100,9 @@ type OIDCProviderStatus struct { // +optional LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` - // JWKSSecret holds the name of the secret in which this OIDC Provider's signing/verification keys - // are stored. If it is empty, then the signing/verification keys are either unknown or they don't - // exist. + // Secrets contains information about this OIDC Provider's secrets. // +optional - JWKSSecret corev1.LocalObjectReference `json:"jwksSecret,omitempty"` + Secrets OIDCProviderSecrets `json:"secrets,omitempty"` } // OIDCProvider describes the configuration of an OIDC provider. diff --git a/generated/1.18/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.18/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index f208d4d0..0cfc17a4 100644 --- a/generated/1.18/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.18/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -72,6 +72,26 @@ func (in *OIDCProviderList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCProviderSecrets) DeepCopyInto(out *OIDCProviderSecrets) { + *out = *in + out.JWKS = in.JWKS + out.TokenSigningKey = in.TokenSigningKey + out.StateSigningKey = in.StateSigningKey + out.StateEncryptionKey = in.StateEncryptionKey + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCProviderSecrets. +func (in *OIDCProviderSecrets) DeepCopy() *OIDCProviderSecrets { + if in == nil { + return nil + } + out := new(OIDCProviderSecrets) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCProviderSpec) DeepCopyInto(out *OIDCProviderSpec) { *out = *in @@ -100,7 +120,7 @@ func (in *OIDCProviderStatus) DeepCopyInto(out *OIDCProviderStatus) { in, out := &in.LastUpdateTime, &out.LastUpdateTime *out = (*in).DeepCopy() } - out.JWKSSecret = in.JWKSSecret + out.Secrets = in.Secrets return } diff --git a/generated/1.18/crds/config.supervisor.pinniped.dev_oidcproviders.yaml b/generated/1.18/crds/config.supervisor.pinniped.dev_oidcproviders.yaml index 6ff3a42b..18771821 100644 --- a/generated/1.18/crds/config.supervisor.pinniped.dev_oidcproviders.yaml +++ b/generated/1.18/crds/config.supervisor.pinniped.dev_oidcproviders.yaml @@ -81,17 +81,6 @@ spec: status: description: Status of the OIDC provider. properties: - jwksSecret: - description: JWKSSecret holds the name of the secret in which this - OIDC Provider's signing/verification keys are stored. If it is empty, - then the signing/verification keys are either unknown or they don't - exist. - properties: - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - type: object lastUpdateTime: description: LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior @@ -101,6 +90,51 @@ spec: message: description: Message provides human-readable details about the Status. type: string + secrets: + description: Secrets contains information about this OIDC Provider's + secrets. + properties: + jwks: + description: JWKS holds the name of the corev1.Secret in which + this OIDC Provider's signing/verification keys are stored. If + it is empty, then the signing/verification keys are either unknown + or they don't exist. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + stateEncryptionKey: + description: StateSigningKey holds the name of the corev1.Secret + in which this OIDC Provider's key for encrypting state parameters + is stored. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + stateSigningKey: + description: StateSigningKey holds the name of the corev1.Secret + in which this OIDC Provider's key for signing state parameters + is stored. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + tokenSigningKey: + description: TokenSigningKey holds the name of the corev1.Secret + in which this OIDC Provider's key for signing tokens is stored. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + type: object status: description: Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure. diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index edda33b8..ec9176fe 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -304,6 +304,26 @@ OIDCProvider describes the configuration of an OIDC provider. +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-config-v1alpha1-oidcprovidersecrets"] +==== OIDCProviderSecrets + +OIDCProviderSecrets holds information about this OIDC Provider's secrets. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-config-v1alpha1-oidcproviderstatus[$$OIDCProviderStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`jwks`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | JWKS holds the name of the corev1.Secret in which this OIDC Provider's signing/verification keys are stored. If it is empty, then the signing/verification keys are either unknown or they don't exist. +| *`tokenSigningKey`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | TokenSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for signing tokens is stored. +| *`stateSigningKey`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for signing state parameters is stored. +| *`stateEncryptionKey`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for encrypting state parameters is stored. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-config-v1alpha1-oidcproviderspec"] ==== OIDCProviderSpec @@ -339,7 +359,7 @@ OIDCProviderStatus is a struct that describes the actual state of an OIDC Provid | *`status`* __OIDCProviderStatusCondition__ | Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure. | *`message`* __string__ | Message provides human-readable details about the Status. | *`lastUpdateTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#time-v1-meta[$$Time$$]__ | LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). -| *`jwksSecret`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | JWKSSecret holds the name of the secret in which this OIDC Provider's signing/verification keys are stored. If it is empty, then the signing/verification keys are either unknown or they don't exist. +| *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-config-v1alpha1-oidcprovidersecrets[$$OIDCProviderSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. |=== diff --git a/generated/1.19/apis/supervisor/config/v1alpha1/types_oidcprovider.go b/generated/1.19/apis/supervisor/config/v1alpha1/types_oidcprovider.go index 908470f0..2e31219e 100644 --- a/generated/1.19/apis/supervisor/config/v1alpha1/types_oidcprovider.go +++ b/generated/1.19/apis/supervisor/config/v1alpha1/types_oidcprovider.go @@ -59,6 +59,30 @@ type OIDCProviderSpec struct { TLS *OIDCProviderTLSSpec `json:"tls,omitempty"` } +// OIDCProviderSecrets holds information about this OIDC Provider's secrets. +type OIDCProviderSecrets struct { + // JWKS holds the name of the corev1.Secret in which this OIDC Provider's signing/verification keys are + // stored. If it is empty, then the signing/verification keys are either unknown or they don't + // exist. + // +optional + JWKS corev1.LocalObjectReference `json:"jwks,omitempty"` + + // TokenSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for + // signing tokens is stored. + // +optional + TokenSigningKey corev1.LocalObjectReference `json:"tokenSigningKey,omitempty"` + + // StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for + // signing state parameters is stored. + // +optional + StateSigningKey corev1.LocalObjectReference `json:"stateSigningKey,omitempty"` + + // StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for + // encrypting state parameters is stored. + // +optional + StateEncryptionKey corev1.LocalObjectReference `json:"stateEncryptionKey,omitempty"` +} + // OIDCProviderStatus is a struct that describes the actual state of an OIDC Provider. type OIDCProviderStatus struct { // Status holds an enum that describes the state of this OIDC Provider. Note that this Status can @@ -76,11 +100,9 @@ type OIDCProviderStatus struct { // +optional LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` - // JWKSSecret holds the name of the secret in which this OIDC Provider's signing/verification keys - // are stored. If it is empty, then the signing/verification keys are either unknown or they don't - // exist. + // Secrets contains information about this OIDC Provider's secrets. // +optional - JWKSSecret corev1.LocalObjectReference `json:"jwksSecret,omitempty"` + Secrets OIDCProviderSecrets `json:"secrets,omitempty"` } // OIDCProvider describes the configuration of an OIDC provider. diff --git a/generated/1.19/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.19/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index f208d4d0..0cfc17a4 100644 --- a/generated/1.19/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.19/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -72,6 +72,26 @@ func (in *OIDCProviderList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCProviderSecrets) DeepCopyInto(out *OIDCProviderSecrets) { + *out = *in + out.JWKS = in.JWKS + out.TokenSigningKey = in.TokenSigningKey + out.StateSigningKey = in.StateSigningKey + out.StateEncryptionKey = in.StateEncryptionKey + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCProviderSecrets. +func (in *OIDCProviderSecrets) DeepCopy() *OIDCProviderSecrets { + if in == nil { + return nil + } + out := new(OIDCProviderSecrets) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCProviderSpec) DeepCopyInto(out *OIDCProviderSpec) { *out = *in @@ -100,7 +120,7 @@ func (in *OIDCProviderStatus) DeepCopyInto(out *OIDCProviderStatus) { in, out := &in.LastUpdateTime, &out.LastUpdateTime *out = (*in).DeepCopy() } - out.JWKSSecret = in.JWKSSecret + out.Secrets = in.Secrets return } diff --git a/generated/1.19/crds/config.supervisor.pinniped.dev_oidcproviders.yaml b/generated/1.19/crds/config.supervisor.pinniped.dev_oidcproviders.yaml index 6ff3a42b..18771821 100644 --- a/generated/1.19/crds/config.supervisor.pinniped.dev_oidcproviders.yaml +++ b/generated/1.19/crds/config.supervisor.pinniped.dev_oidcproviders.yaml @@ -81,17 +81,6 @@ spec: status: description: Status of the OIDC provider. properties: - jwksSecret: - description: JWKSSecret holds the name of the secret in which this - OIDC Provider's signing/verification keys are stored. If it is empty, - then the signing/verification keys are either unknown or they don't - exist. - properties: - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - type: object lastUpdateTime: description: LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior @@ -101,6 +90,51 @@ spec: message: description: Message provides human-readable details about the Status. type: string + secrets: + description: Secrets contains information about this OIDC Provider's + secrets. + properties: + jwks: + description: JWKS holds the name of the corev1.Secret in which + this OIDC Provider's signing/verification keys are stored. If + it is empty, then the signing/verification keys are either unknown + or they don't exist. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + stateEncryptionKey: + description: StateSigningKey holds the name of the corev1.Secret + in which this OIDC Provider's key for encrypting state parameters + is stored. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + stateSigningKey: + description: StateSigningKey holds the name of the corev1.Secret + in which this OIDC Provider's key for signing state parameters + is stored. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + tokenSigningKey: + description: TokenSigningKey holds the name of the corev1.Secret + in which this OIDC Provider's key for signing tokens is stored. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + type: object status: description: Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure. diff --git a/internal/controller/supervisorconfig/generator/generator.go b/internal/controller/supervisorconfig/generator/generator.go index 3aeed955..26328caa 100644 --- a/internal/controller/supervisorconfig/generator/generator.go +++ b/internal/controller/supervisorconfig/generator/generator.go @@ -27,11 +27,11 @@ func generateSymmetricKey() ([]byte, error) { } func isValid(secret *corev1.Secret) bool { - if secret.Type != SymmetricSecretType { + if secret.Type != symmetricSecretType { return false } - data, ok := secret.Data[SymmetricSecretDataKey] + data, ok := secret.Data[symmetricSecretDataKey] if !ok { return false } @@ -49,7 +49,7 @@ func secretDataFunc() (map[string][]byte, error) { } return map[string][]byte{ - SymmetricSecretDataKey: symmetricKey, + symmetricSecretDataKey: symmetricKey, }, nil } @@ -73,7 +73,7 @@ func generateSecret(namespace, name string, labels map[string]string, secretData }, Labels: labels, }, - Type: SymmetricSecretType, + Type: symmetricSecretType, Data: secretData, }, nil } diff --git a/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go index 01f0c491..aeada106 100644 --- a/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go +++ b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go @@ -6,6 +6,7 @@ package generator import ( "context" "fmt" + "reflect" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -16,6 +17,7 @@ import ( "k8s.io/klog/v2" configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1" + pinnipedclientset "go.pinniped.dev/generated/1.19/client/supervisor/clientset/versioned" configinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions/config/v1alpha1" pinnipedcontroller "go.pinniped.dev/internal/controller" "go.pinniped.dev/internal/controllerlib" @@ -25,6 +27,7 @@ import ( type oidcProviderSecretsController struct { secretHelper SecretHelper kubeClient kubernetes.Interface + pinnipedClient pinnipedclientset.Interface opcInformer configinformers.OIDCProviderInformer secretInformer corev1informers.SecretInformer } @@ -35,6 +38,7 @@ type oidcProviderSecretsController struct { func NewOIDCProviderSecretsController( secretHelper SecretHelper, kubeClient kubernetes.Interface, + pinnipedClient pinnipedclientset.Interface, secretInformer corev1informers.SecretInformer, opcInformer configinformers.OIDCProviderInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, @@ -45,6 +49,7 @@ func NewOIDCProviderSecretsController( Syncer: &oidcProviderSecretsController{ secretHelper: secretHelper, kubeClient: kubeClient, + pinnipedClient: pinnipedClient, secretInformer: secretInformer, opcInformer: opcInformer, }, @@ -116,7 +121,13 @@ func (c *oidcProviderSecretsController) Sync(ctx controllerlib.Context) error { "secret", klog.KObj(existingSecret), ) - c.secretHelper.Notify(op, existingSecret) + + op = c.secretHelper.ObserveActiveSecretAndUpdateParentOIDCProvider(op, existingSecret) + if err := c.updateOIDCProvider(ctx.Context, op); err != nil { + return fmt.Errorf("failed to update oidcprovider: %w", err) + } + plog.Debug("updated oidcprovider", "oidcprovider", klog.KObj(op), "secret", klog.KObj(newSecret)) + return nil } @@ -127,7 +138,11 @@ func (c *oidcProviderSecretsController) Sync(ctx controllerlib.Context) error { } plog.Debug("created/updated secret", "oidcprovider", klog.KObj(op), "secret", klog.KObj(newSecret)) - c.secretHelper.Notify(op, newSecret) + op = c.secretHelper.ObserveActiveSecretAndUpdateParentOIDCProvider(op, newSecret) + if err := c.updateOIDCProvider(ctx.Context, op); err != nil { + return fmt.Errorf("failed to update oidcprovider: %w", err) + } + plog.Debug("updated oidcprovider", "oidcprovider", klog.KObj(op), "secret", klog.KObj(newSecret)) return nil } @@ -195,3 +210,24 @@ func (c *oidcProviderSecretsController) createOrUpdateSecret( return err }) } + +func (c *oidcProviderSecretsController) updateOIDCProvider( + ctx context.Context, + newOP *configv1alpha1.OIDCProvider, +) error { + opcClient := c.pinnipedClient.ConfigV1alpha1().OIDCProviders(newOP.Namespace) + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + oldOP, err := opcClient.Get(ctx, newOP.Name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get oidcprovider %s/%s: %w", newOP.Namespace, newOP.Name, err) + } + + if reflect.DeepEqual(newOP.Status.Secrets, oldOP.Status.Secrets) { + return nil + } + + oldOP.Status.Secrets = newOP.Status.Secrets + _, err = opcClient.Update(ctx, oldOP, metav1.UpdateOptions{}) + return err + }) +} diff --git a/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go b/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go index 68cbc4bf..e888c7c2 100644 --- a/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go +++ b/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go @@ -158,6 +158,7 @@ func TestOIDCProviderControllerFilterSecret(t *testing.T) { _ = NewOIDCProviderSecretsController( secretHelper, nil, // kubeClient, not needed + nil, // pinnipedClient, not needed secretInformer, opcInformer, withInformer.WithInformer, @@ -216,6 +217,7 @@ func TestNewOIDCProviderSecretsControllerFilterOPC(t *testing.T) { _ = NewOIDCProviderSecretsController( secretHelper, nil, // kubeClient, not needed + nil, // pinnipedClient, not needed secretInformer, opcInformer, withInformer.WithInformer, @@ -291,6 +293,9 @@ func TestOIDCProviderSecretsControllerSync(t *testing.T) { }, } + goodOPWithStatus := goodOP.DeepCopy() + goodOPWithStatus.Status.Secrets.TokenSigningKey.Name = goodSecret.Name + invalidSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: secretName, @@ -309,26 +314,24 @@ func TestOIDCProviderSecretsControllerSync(t *testing.T) { }, } - once := sync.Once{} - tests := []struct { name string storage func(**configv1alpha1.OIDCProvider, **corev1.Secret) - client func(*kubernetesfake.Clientset) + client func(*pinnipedfake.Clientset, *kubernetesfake.Clientset) secretHelper func(*mocksecrethelper.MockSecretHelper) - wantSecretActions []kubetesting.Action wantOPActions []kubetesting.Action + wantSecretActions []kubetesting.Action wantError string }{ { - name: "OIDCProvider exists and secret does not exist", + name: "OIDCProvider does not exist and secret does not exist", storage: func(op **configv1alpha1.OIDCProvider, s **corev1.Secret) { *op = nil *s = nil }, }, { - name: "OIDCProvider exists and secret exists", + name: "OIDCProvider does not exist and secret exists", storage: func(op **configv1alpha1.OIDCProvider, s **corev1.Secret) { *op = nil }, @@ -340,21 +343,17 @@ func TestOIDCProviderSecretsControllerSync(t *testing.T) { }, secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { secretHelper.EXPECT().Generate(goodOP).Times(1).Return(goodSecret, nil) - secretHelper.EXPECT().Notify(goodOP, goodSecret).Times(1) + secretHelper.EXPECT().ObserveActiveSecretAndUpdateParentOIDCProvider(goodOP, goodSecret).Times(1).Return(goodOPWithStatus) + }, + wantOPActions: []kubetesting.Action{ + kubetesting.NewGetAction(opGVR, namespace, goodOP.Name), + kubetesting.NewUpdateAction(opGVR, namespace, goodOPWithStatus), }, wantSecretActions: []kubetesting.Action{ kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), kubetesting.NewCreateAction(secretGVR, namespace, goodSecret), }, }, - { - name: "OIDCProvider exists and valid secret exists", - secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { - secretHelper.EXPECT().Generate(goodOP).Times(1).Return(goodSecret, nil) - secretHelper.EXPECT().IsValid(goodOP, goodSecret).Times(1).Return(true) - secretHelper.EXPECT().Notify(goodOP, goodSecret).Times(1) - }, - }, { name: "OIDCProvider exists and invalid secret exists", storage: func(op **configv1alpha1.OIDCProvider, s **corev1.Secret) { @@ -363,7 +362,11 @@ func TestOIDCProviderSecretsControllerSync(t *testing.T) { secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { secretHelper.EXPECT().Generate(goodOP).Times(1).Return(goodSecret, nil) secretHelper.EXPECT().IsValid(goodOP, invalidSecret).Times(2).Return(false) - secretHelper.EXPECT().Notify(goodOP, goodSecret).Times(1) + secretHelper.EXPECT().ObserveActiveSecretAndUpdateParentOIDCProvider(goodOP, goodSecret).Times(1).Return(goodOPWithStatus) + }, + wantOPActions: []kubetesting.Action{ + kubetesting.NewGetAction(opGVR, namespace, goodOP.Name), + kubetesting.NewUpdateAction(opGVR, namespace, goodOPWithStatus), }, wantSecretActions: []kubetesting.Action{ kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), @@ -386,19 +389,23 @@ func TestOIDCProviderSecretsControllerSync(t *testing.T) { secretHelper.EXPECT().Generate(goodOP).Times(1).Return(otherSecret, nil) secretHelper.EXPECT().IsValid(goodOP, goodSecret).Times(1).Return(false) secretHelper.EXPECT().IsValid(goodOP, goodSecret).Times(1).Return(true) - secretHelper.EXPECT().Notify(goodOP, goodSecret).Times(1) + secretHelper.EXPECT().ObserveActiveSecretAndUpdateParentOIDCProvider(goodOP, goodSecret).Times(1).Return(goodOPWithStatus) + }, + wantOPActions: []kubetesting.Action{ + kubetesting.NewGetAction(opGVR, namespace, goodOP.Name), + kubetesting.NewUpdateAction(opGVR, namespace, goodOPWithStatus), }, wantSecretActions: []kubetesting.Action{ kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), }, }, { - name: "OIDCProvider exists and invalid secret exists and get fails", + name: "OIDCProvider exists and invalid secret exists and getting secret fails", secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { secretHelper.EXPECT().Generate(goodOP).Times(1).Return(goodSecret, nil) secretHelper.EXPECT().IsValid(goodOP, goodSecret).Times(1).Return(false) }, - client: func(c *kubernetesfake.Clientset) { + client: func(_ *pinnipedfake.Clientset, c *kubernetesfake.Clientset) { c.PrependReactor("get", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { return true, nil, errors.New("some get error") }) @@ -409,14 +416,14 @@ func TestOIDCProviderSecretsControllerSync(t *testing.T) { wantError: fmt.Sprintf("failed to create or update secret: failed to get secret %s/%s: some get error", namespace, goodSecret.Name), }, { - name: "OIDCProvider exists and secret does not exist and create fails", + name: "OIDCProvider exists and secret does not exist and creating secret fails", storage: func(op **configv1alpha1.OIDCProvider, s **corev1.Secret) { *s = nil }, secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { secretHelper.EXPECT().Generate(goodOP).Times(1).Return(goodSecret, nil) }, - client: func(c *kubernetesfake.Clientset) { + client: func(_ *pinnipedfake.Clientset, c *kubernetesfake.Clientset) { c.PrependReactor("create", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { return true, nil, errors.New("some create error") }) @@ -428,12 +435,12 @@ func TestOIDCProviderSecretsControllerSync(t *testing.T) { wantError: fmt.Sprintf("failed to create or update secret: failed to create secret %s/%s: some create error", namespace, goodSecret.Name), }, { - name: "OIDCProvider exists and invalid secret exists and update fails", + name: "OIDCProvider exists and invalid secret exists and updating secret fails", secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { secretHelper.EXPECT().Generate(goodOP).Times(1).Return(goodSecret, nil) secretHelper.EXPECT().IsValid(goodOP, goodSecret).Times(2).Return(false) }, - client: func(c *kubernetesfake.Clientset) { + client: func(_ *pinnipedfake.Clientset, c *kubernetesfake.Clientset) { c.PrependReactor("update", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { return true, nil, errors.New("some update error") }) @@ -445,22 +452,27 @@ func TestOIDCProviderSecretsControllerSync(t *testing.T) { wantError: "failed to create or update secret: some update error", }, { - name: "OIDCProvider exists and invalid secret exists and update fails due to conflict", + name: "OIDCProvider exists and invalid secret exists and updating secret fails due to conflict", storage: func(op **configv1alpha1.OIDCProvider, s **corev1.Secret) { *s = invalidSecret.DeepCopy() }, secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { secretHelper.EXPECT().Generate(goodOP).Times(1).Return(goodSecret, nil) secretHelper.EXPECT().IsValid(goodOP, invalidSecret).Times(3).Return(false) - secretHelper.EXPECT().Notify(goodOP, goodSecret).Times(1) + secretHelper.EXPECT().ObserveActiveSecretAndUpdateParentOIDCProvider(goodOP, goodSecret).Times(1).Return(goodOPWithStatus) }, - client: func(c *kubernetesfake.Clientset) { + client: func(_ *pinnipedfake.Clientset, c *kubernetesfake.Clientset) { + once := sync.Once{} c.PrependReactor("update", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { var err error once.Do(func() { err = k8serrors.NewConflict(secretGVR.GroupResource(), namespace, errors.New("some error")) }) return true, nil, err }) }, + wantOPActions: []kubetesting.Action{ + kubetesting.NewGetAction(opGVR, namespace, goodOP.Name), + kubetesting.NewUpdateAction(opGVR, namespace, goodOPWithStatus), + }, wantSecretActions: []kubetesting.Action{ kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret), @@ -468,6 +480,59 @@ func TestOIDCProviderSecretsControllerSync(t *testing.T) { kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret), }, }, + { + name: "OIDCProvider exists and invalid secret exists and getting OIDCProvider fails", + storage: func(op **configv1alpha1.OIDCProvider, s **corev1.Secret) { + *s = invalidSecret.DeepCopy() + }, + secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { + secretHelper.EXPECT().Generate(goodOP).Times(1).Return(goodSecret, nil) + secretHelper.EXPECT().IsValid(goodOP, invalidSecret).Times(2).Return(false) + secretHelper.EXPECT().ObserveActiveSecretAndUpdateParentOIDCProvider(goodOP, goodSecret).Times(1).Return(goodOPWithStatus) + }, + client: func(c *pinnipedfake.Clientset, _ *kubernetesfake.Clientset) { + c.PrependReactor("get", "oidcproviders", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("some get error") + }) + }, + wantOPActions: []kubetesting.Action{ + kubetesting.NewGetAction(opGVR, namespace, goodOP.Name), + }, + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret), + }, + wantError: fmt.Sprintf("failed to update oidcprovider: failed to get oidcprovider %s/%s: some get error", goodOPWithStatus.Namespace, goodOPWithStatus.Name), + }, + { + name: "OIDCProvider exists and invalid secret exists and updating OIDCProvider fails due to conflict", + storage: func(op **configv1alpha1.OIDCProvider, s **corev1.Secret) { + *s = invalidSecret.DeepCopy() + }, + secretHelper: func(secretHelper *mocksecrethelper.MockSecretHelper) { + secretHelper.EXPECT().Generate(goodOP).Times(1).Return(goodSecret, nil) + secretHelper.EXPECT().IsValid(goodOP, invalidSecret).Times(2).Return(false) + secretHelper.EXPECT().ObserveActiveSecretAndUpdateParentOIDCProvider(goodOP, goodSecret).Times(1).Return(goodOPWithStatus) + }, + client: func(c *pinnipedfake.Clientset, _ *kubernetesfake.Clientset) { + once := sync.Once{} + c.PrependReactor("update", "oidcproviders", func(_ kubetesting.Action) (bool, runtime.Object, error) { + var err error + once.Do(func() { err = k8serrors.NewConflict(secretGVR.GroupResource(), namespace, errors.New("some error")) }) + return true, nil, err + }) + }, + wantOPActions: []kubetesting.Action{ + kubetesting.NewGetAction(opGVR, namespace, goodOP.Name), + kubetesting.NewUpdateAction(opGVR, namespace, goodOPWithStatus), + kubetesting.NewGetAction(opGVR, namespace, goodOP.Name), + kubetesting.NewUpdateAction(opGVR, namespace, goodOPWithStatus), + }, + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret), + }, + }, } for _, test := range tests { test := test @@ -477,6 +542,7 @@ func TestOIDCProviderSecretsControllerSync(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) defer cancel() + pinnipedAPIClient := pinnipedfake.NewSimpleClientset() pinnipedInformerClient := pinnipedfake.NewSimpleClientset() kubeAPIClient := kubernetesfake.NewSimpleClientset() @@ -488,6 +554,7 @@ func TestOIDCProviderSecretsControllerSync(t *testing.T) { test.storage(&op, &secret) } if op != nil { + require.NoError(t, pinnipedAPIClient.Tracker().Add(op)) require.NoError(t, pinnipedInformerClient.Tracker().Add(op)) } if secret != nil { @@ -496,7 +563,7 @@ func TestOIDCProviderSecretsControllerSync(t *testing.T) { } if test.client != nil { - test.client(kubeAPIClient) + test.client(pinnipedAPIClient, kubeAPIClient) } kubeInformers := kubeinformers.NewSharedInformerFactory( @@ -519,6 +586,7 @@ func TestOIDCProviderSecretsControllerSync(t *testing.T) { c := NewOIDCProviderSecretsController( secretHelper, kubeAPIClient, + pinnipedAPIClient, kubeInformers.Core().V1().Secrets(), pinnipedInformers.Config().V1alpha1().OIDCProviders(), controllerlib.WithInformer, @@ -542,6 +610,10 @@ func TestOIDCProviderSecretsControllerSync(t *testing.T) { } require.NoError(t, err) + if test.wantOPActions == nil { + test.wantOPActions = []kubetesting.Action{} + } + require.Equal(t, test.wantOPActions, pinnipedAPIClient.Actions()) if test.wantSecretActions == nil { test.wantSecretActions = []kubetesting.Action{} } diff --git a/internal/controller/supervisorconfig/generator/secret_helper.go b/internal/controller/supervisorconfig/generator/secret_helper.go index cfbcb5a7..3a3362a6 100644 --- a/internal/controller/supervisorconfig/generator/secret_helper.go +++ b/internal/controller/supervisorconfig/generator/secret_helper.go @@ -12,6 +12,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1" + "go.pinniped.dev/internal/plog" ) // SecretHelper describes an object that can Generate() a Secret and determine whether a Secret @@ -22,14 +23,14 @@ type SecretHelper interface { NamePrefix() string Generate(*configv1alpha1.OIDCProvider) (*corev1.Secret, error) IsValid(*configv1alpha1.OIDCProvider, *corev1.Secret) bool - Notify(*configv1alpha1.OIDCProvider, *corev1.Secret) + ObserveActiveSecretAndUpdateParentOIDCProvider(*configv1alpha1.OIDCProvider, *corev1.Secret) *configv1alpha1.OIDCProvider } const ( - // SymmetricSecretType is corev1.Secret.Type of all corev1.Secret's generated by this helper. - SymmetricSecretType = "secrets.pinniped.dev/symmetric" - // SymmetricSecretDataKey is the corev1.Secret.Data key for the symmetric key value generated by this helper. - SymmetricSecretDataKey = "key" + // symmetricSecretType is corev1.Secret.Type of all corev1.Secret's generated by this helper. + symmetricSecretType = "secrets.pinniped.dev/symmetric" + // symmetricSecretDataKey is the corev1.Secret.Data key for the symmetric key value generated by this helper. + symmetricSecretDataKey = "key" // symmetricKeySize is the default length, in bytes, of generated keys. It is set to 32 since this // seems like reasonable entropy for our keys, and a 32-byte key will allow for AES-256 @@ -37,18 +38,30 @@ const ( symmetricKeySize = 32 ) +// SecretUsage describes how a cryptographic secret is going to be used. It is currently used to +// indicate to a SecretHelper which status field to set on the parent OIDCProvider for a Secret. +type SecretUsage int + +const ( + SecretUsageTokenSigningKey SecretUsage = iota + SecretUsageStateSigningKey + SecretUsageStateEncryptionKey +) + // New returns a SecretHelper that has been parameterized with common symmetric secret generation // knobs. func NewSymmetricSecretHelper( namePrefix string, labels map[string]string, rand io.Reader, + secretUsage SecretUsage, updateCacheFunc func(cacheKey string, cacheValue []byte), ) SecretHelper { return &symmetricSecretHelper{ namePrefix: namePrefix, labels: labels, rand: rand, + secretUsage: secretUsage, updateCacheFunc: updateCacheFunc, } } @@ -57,6 +70,7 @@ type symmetricSecretHelper struct { namePrefix string labels map[string]string rand io.Reader + secretUsage SecretUsage updateCacheFunc func(cacheKey string, cacheValue []byte) } @@ -82,9 +96,9 @@ func (s *symmetricSecretHelper) Generate(parent *configv1alpha1.OIDCProvider) (* }), }, }, - Type: SymmetricSecretType, + Type: symmetricSecretType, Data: map[string][]byte{ - SymmetricSecretDataKey: key, + symmetricSecretDataKey: key, }, }, nil } @@ -95,11 +109,11 @@ func (s *symmetricSecretHelper) IsValid(parent *configv1alpha1.OIDCProvider, sec return false } - if secret.Type != SymmetricSecretType { + if secret.Type != symmetricSecretType { return false } - key, ok := secret.Data[SymmetricSecretDataKey] + key, ok := secret.Data[symmetricSecretDataKey] if !ok { return false } @@ -110,12 +124,28 @@ func (s *symmetricSecretHelper) IsValid(parent *configv1alpha1.OIDCProvider, sec return true } -// Notify implements SecretHelper.Notify(). -func (s *symmetricSecretHelper) Notify(op *configv1alpha1.OIDCProvider, secret *corev1.Secret) { +// ObserveActiveSecretAndUpdateParentOIDCProvider implements SecretHelper.ObserveActiveSecretAndUpdateParentOIDCProvider(). +func (s *symmetricSecretHelper) ObserveActiveSecretAndUpdateParentOIDCProvider( + op *configv1alpha1.OIDCProvider, + secret *corev1.Secret, +) *configv1alpha1.OIDCProvider { var cacheKey string if op != nil { cacheKey = op.Spec.Issuer } - s.updateCacheFunc(cacheKey, secret.Data[SymmetricSecretDataKey]) + s.updateCacheFunc(cacheKey, secret.Data[symmetricSecretDataKey]) + + switch s.secretUsage { + case SecretUsageTokenSigningKey: + op.Status.Secrets.TokenSigningKey.Name = secret.Name + case SecretUsageStateSigningKey: + op.Status.Secrets.StateSigningKey.Name = secret.Name + case SecretUsageStateEncryptionKey: + op.Status.Secrets.StateEncryptionKey.Name = secret.Name + default: + plog.Warning("unknown secret usage enum value: %d", s.secretUsage) + } + + return op } diff --git a/internal/controller/supervisorconfig/generator/secret_helper_test.go b/internal/controller/supervisorconfig/generator/secret_helper_test.go index ffe89241..bbaeb4e1 100644 --- a/internal/controller/supervisorconfig/generator/secret_helper_test.go +++ b/internal/controller/supervisorconfig/generator/secret_helper_test.go @@ -17,57 +17,98 @@ import ( const keyWith32Bytes = "0123456789abcdef0123456789abcdef" -func TestSymmetricSecretHHelper(t *testing.T) { - labels := map[string]string{ - "some-label-key-1": "some-label-value-1", - "some-label-key-2": "some-label-value-2", - } - randSource := strings.NewReader(keyWith32Bytes) - // var notifyParent *configv1alpha1.OIDCProvider - // var notifyChild *corev1.Secret - var oidcProviderIssuerValue string - var symmetricKeyValue []byte - h := NewSymmetricSecretHelper("some-name-prefix-", labels, randSource, func(oidcProviderIssuer string, symmetricKey []byte) { - require.True(t, oidcProviderIssuer == "" && symmetricKeyValue == nil, "expected notify func not to have been called yet") - oidcProviderIssuerValue = oidcProviderIssuer - symmetricKeyValue = symmetricKey - }) +func TestSymmetricSecretHelper(t *testing.T) { + t.Parallel() - parent := &configv1alpha1.OIDCProvider{ - ObjectMeta: metav1.ObjectMeta{ - UID: "some-uid", - Namespace: "some-namespace", - }, - } - child, err := h.Generate(parent) - require.NoError(t, err) - require.Equal(t, child, &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "some-name-prefix-some-uid", - Namespace: "some-namespace", - Labels: labels, - OwnerReferences: []metav1.OwnerReference{ - *metav1.NewControllerRef(parent, schema.GroupVersionKind{ - Group: configv1alpha1.SchemeGroupVersion.Group, - Version: configv1alpha1.SchemeGroupVersion.Version, - Kind: "OIDCProvider", - }), + tests := []struct { + name string + secretUsage SecretUsage + wantSetOIDCProviderField func(*configv1alpha1.OIDCProvider) string + }{ + { + name: "token signing key", + secretUsage: SecretUsageTokenSigningKey, + wantSetOIDCProviderField: func(op *configv1alpha1.OIDCProvider) string { + return op.Status.Secrets.TokenSigningKey.Name }, }, - Type: "secrets.pinniped.dev/symmetric", - Data: map[string][]byte{ - "key": []byte(keyWith32Bytes), + { + name: "state signing key", + secretUsage: SecretUsageStateSigningKey, + wantSetOIDCProviderField: func(op *configv1alpha1.OIDCProvider) string { + return op.Status.Secrets.StateSigningKey.Name + }, }, - }) + { + name: "state encryption key", + secretUsage: SecretUsageStateEncryptionKey, + wantSetOIDCProviderField: func(op *configv1alpha1.OIDCProvider) string { + return op.Status.Secrets.StateEncryptionKey.Name + }, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() - require.True(t, h.IsValid(parent, child)) + labels := map[string]string{ + "some-label-key-1": "some-label-value-1", + "some-label-key-2": "some-label-value-2", + } + randSource := strings.NewReader(keyWith32Bytes) + var oidcProviderIssuerValue string + var symmetricKeyValue []byte + h := NewSymmetricSecretHelper( + "some-name-prefix-", + labels, + randSource, + test.secretUsage, + func(oidcProviderIssuer string, symmetricKey []byte) { + require.True(t, oidcProviderIssuer == "" && symmetricKeyValue == nil, "expected notify func not to have been called yet") + oidcProviderIssuerValue = oidcProviderIssuer + symmetricKeyValue = symmetricKey + }, + ) - h.Notify(parent, child) - require.Equal(t, parent.Spec.Issuer, oidcProviderIssuerValue) - require.Equal(t, child.Data[SymmetricSecretDataKey], symmetricKeyValue) + parent := &configv1alpha1.OIDCProvider{ + ObjectMeta: metav1.ObjectMeta{ + UID: "some-uid", + Namespace: "some-namespace", + }, + } + child, err := h.Generate(parent) + require.NoError(t, err) + require.Equal(t, child, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-name-prefix-some-uid", + Namespace: "some-namespace", + Labels: labels, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(parent, schema.GroupVersionKind{ + Group: configv1alpha1.SchemeGroupVersion.Group, + Version: configv1alpha1.SchemeGroupVersion.Version, + Kind: "OIDCProvider", + }), + }, + }, + Type: "secrets.pinniped.dev/symmetric", + Data: map[string][]byte{ + "key": []byte(keyWith32Bytes), + }, + }) + + require.True(t, h.IsValid(parent, child)) + + h.ObserveActiveSecretAndUpdateParentOIDCProvider(parent, child) + require.Equal(t, parent.Spec.Issuer, oidcProviderIssuerValue) + require.Equal(t, child.Name, test.wantSetOIDCProviderField(parent)) + require.Equal(t, child.Data["key"], symmetricKeyValue) + }) + } } -func TestSymmetricSecretHHelperIsValid(t *testing.T) { +func TestSymmetricSecretHelperIsValid(t *testing.T) { tests := []struct { name string child func(*corev1.Secret) @@ -117,7 +158,7 @@ func TestSymmetricSecretHHelperIsValid(t *testing.T) { for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - h := NewSymmetricSecretHelper("none of these args matter", nil, nil, nil) + h := NewSymmetricSecretHelper("none of these args matter", nil, nil, SecretUsageTokenSigningKey, nil) parent := &configv1alpha1.OIDCProvider{ ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets.go b/internal/controller/supervisorconfig/generator/supervisor_secrets.go index ec89993f..d3155520 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets.go @@ -79,7 +79,7 @@ func (c *supervisorSecretsController) Sync(ctx controllerlib.Context) error { secretNeedsUpdate := isNotFound || !isValid(secret) if !secretNeedsUpdate { plog.Debug("secret is up to date", "secret", klog.KObj(secret)) - c.setCacheFunc(secret.Data[SymmetricSecretDataKey]) + c.setCacheFunc(secret.Data[symmetricSecretDataKey]) return nil } @@ -97,7 +97,7 @@ func (c *supervisorSecretsController) Sync(ctx controllerlib.Context) error { return fmt.Errorf("failed to create/update secret %s/%s: %w", newSecret.Namespace, newSecret.Name, err) } - c.setCacheFunc(newSecret.Data[SymmetricSecretDataKey]) + c.setCacheFunc(newSecret.Data[symmetricSecretDataKey]) return nil } diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go b/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go index c5357ce1..307e1f66 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go @@ -194,8 +194,8 @@ func TestSupervisorSecretsControllerInitialEvent(t *testing.T) { initialEventOption.WithInitialEvent, ) require.Equal(t, &controllerlib.Key{ - owner.Namespace, - owner.Name + "-key", + Namespace: owner.Namespace, + Name: owner.Name + "-key", }, initialEventOption.GetInitialEventKey()) } diff --git a/internal/controller/supervisorconfig/jwks_observer.go b/internal/controller/supervisorconfig/jwks_observer.go index 5c01c5b4..efbdfae9 100644 --- a/internal/controller/supervisorconfig/jwks_observer.go +++ b/internal/controller/supervisorconfig/jwks_observer.go @@ -76,7 +76,7 @@ func (c *jwksObserverController) Sync(ctx controllerlib.Context) error { issuerToActiveJWKMap := map[string]*jose.JSONWebKey{} for _, provider := range allProviders { - secretRef := provider.Status.JWKSSecret + secretRef := provider.Status.Secrets.JWKS jwksSecret, err := c.secretInformer.Lister().Secrets(ns).Get(secretRef.Name) if err != nil { plog.Debug("jwksObserverController Sync could not find JWKS secret", "namespace", ns, "secretName", secretRef.Name) diff --git a/internal/controller/supervisorconfig/jwks_observer_test.go b/internal/controller/supervisorconfig/jwks_observer_test.go index ad7323b8..fa2ebea1 100644 --- a/internal/controller/supervisorconfig/jwks_observer_test.go +++ b/internal/controller/supervisorconfig/jwks_observer_test.go @@ -202,7 +202,7 @@ func TestJWKSObserverControllerSync(t *testing.T) { Namespace: installedInNamespace, }, Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://no-secret-issuer1.com"}, - Status: v1alpha1.OIDCProviderStatus{}, // no JWKSSecret field + Status: v1alpha1.OIDCProviderStatus{}, // no Secrets.JWKS field } oidcProviderWithoutSecret2 := &v1alpha1.OIDCProvider{ ObjectMeta: metav1.ObjectMeta{ @@ -219,7 +219,9 @@ func TestJWKSObserverControllerSync(t *testing.T) { }, Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://bad-secret-issuer.com"}, Status: v1alpha1.OIDCProviderStatus{ - JWKSSecret: corev1.LocalObjectReference{Name: "bad-secret-name"}, + Secrets: v1alpha1.OIDCProviderSecrets{ + JWKS: corev1.LocalObjectReference{Name: "bad-secret-name"}, + }, }, } oidcProviderWithBadJWKSSecret := &v1alpha1.OIDCProvider{ @@ -229,7 +231,9 @@ func TestJWKSObserverControllerSync(t *testing.T) { }, Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://bad-jwks-secret-issuer.com"}, Status: v1alpha1.OIDCProviderStatus{ - JWKSSecret: corev1.LocalObjectReference{Name: "bad-jwks-secret-name"}, + Secrets: v1alpha1.OIDCProviderSecrets{ + JWKS: corev1.LocalObjectReference{Name: "bad-jwks-secret-name"}, + }, }, } oidcProviderWithBadActiveJWKSecret := &v1alpha1.OIDCProvider{ @@ -239,7 +243,9 @@ func TestJWKSObserverControllerSync(t *testing.T) { }, Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://bad-active-jwk-secret-issuer.com"}, Status: v1alpha1.OIDCProviderStatus{ - JWKSSecret: corev1.LocalObjectReference{Name: "bad-active-jwk-secret-name"}, + Secrets: v1alpha1.OIDCProviderSecrets{ + JWKS: corev1.LocalObjectReference{Name: "bad-active-jwk-secret-name"}, + }, }, } oidcProviderWithGoodSecret1 := &v1alpha1.OIDCProvider{ @@ -249,7 +255,9 @@ func TestJWKSObserverControllerSync(t *testing.T) { }, Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://issuer-with-good-secret1.com"}, Status: v1alpha1.OIDCProviderStatus{ - JWKSSecret: corev1.LocalObjectReference{Name: "good-jwks-secret-name1"}, + Secrets: v1alpha1.OIDCProviderSecrets{ + JWKS: corev1.LocalObjectReference{Name: "good-jwks-secret-name1"}, + }, }, } oidcProviderWithGoodSecret2 := &v1alpha1.OIDCProvider{ @@ -259,7 +267,9 @@ func TestJWKSObserverControllerSync(t *testing.T) { }, Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://issuer-with-good-secret2.com"}, Status: v1alpha1.OIDCProviderStatus{ - JWKSSecret: corev1.LocalObjectReference{Name: "good-jwks-secret-name2"}, + Secrets: v1alpha1.OIDCProviderSecrets{ + JWKS: corev1.LocalObjectReference{Name: "good-jwks-secret-name2"}, + }, }, } expectedJWK1 = string(readJWKJSON(t, "testdata/public-jwk.json")) diff --git a/internal/controller/supervisorconfig/jwks_writer.go b/internal/controller/supervisorconfig/jwks_writer.go index 4e6a51a1..2a33b77f 100644 --- a/internal/controller/supervisorconfig/jwks_writer.go +++ b/internal/controller/supervisorconfig/jwks_writer.go @@ -169,7 +169,7 @@ func (c *jwksWriterController) Sync(ctx controllerlib.Context) error { // Ensure that the OPC points to the secret. newOPC := opc.DeepCopy() - newOPC.Status.JWKSSecret.Name = secret.Name + newOPC.Status.Secrets.JWKS.Name = secret.Name if err := c.updateOPC(ctx.Context, newOPC); err != nil { return fmt.Errorf("cannot update opc: %w", err) } @@ -179,13 +179,13 @@ func (c *jwksWriterController) Sync(ctx controllerlib.Context) error { } func (c *jwksWriterController) secretNeedsUpdate(opc *configv1alpha1.OIDCProvider) (bool, error) { - if opc.Status.JWKSSecret.Name == "" { + if opc.Status.Secrets.JWKS.Name == "" { // If the OPC says it doesn't have a secret associated with it, then let's create one. return true, nil } // This OPC says it has a secret associated with it. Let's try to get it from the cache. - secret, err := c.secretInformer.Lister().Secrets(opc.Namespace).Get(opc.Status.JWKSSecret.Name) + secret, err := c.secretInformer.Lister().Secrets(opc.Namespace).Get(opc.Status.Secrets.JWKS.Name) notFound := k8serrors.IsNotFound(err) if err != nil && !notFound { return false, fmt.Errorf("cannot get secret: %w", err) @@ -301,12 +301,12 @@ func (c *jwksWriterController) updateOPC( return fmt.Errorf("cannot get opc: %w", err) } - if newOPC.Status.JWKSSecret.Name == oldOPC.Status.JWKSSecret.Name { + if newOPC.Status.Secrets.JWKS.Name == oldOPC.Status.Secrets.JWKS.Name { // If the existing OPC is up to date, we don't need to update it. return nil } - oldOPC.Status.JWKSSecret.Name = newOPC.Status.JWKSSecret.Name + oldOPC.Status.Secrets.JWKS.Name = newOPC.Status.Secrets.JWKS.Name _, err = opcClient.Update(ctx, oldOPC, metav1.UpdateOptions{}) return err }) diff --git a/internal/controller/supervisorconfig/jwks_writer_test.go b/internal/controller/supervisorconfig/jwks_writer_test.go index 9afdc486..615a1f3d 100644 --- a/internal/controller/supervisorconfig/jwks_writer_test.go +++ b/internal/controller/supervisorconfig/jwks_writer_test.go @@ -253,7 +253,7 @@ func TestJWKSWriterControllerSync(t *testing.T) { }, } goodOPCWithStatus := goodOPC.DeepCopy() - goodOPCWithStatus.Status.JWKSSecret.Name = goodOPCWithStatus.Name + "-jwks" + goodOPCWithStatus.Status.Secrets.JWKS.Name = goodOPCWithStatus.Name + "-jwks" secretGVR := schema.GroupVersionResource{ Group: corev1.SchemeGroupVersion.Group, @@ -264,7 +264,7 @@ func TestJWKSWriterControllerSync(t *testing.T) { newSecret := func(activeJWKPath, jwksPath string) *corev1.Secret { s := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: goodOPCWithStatus.Status.JWKSSecret.Name, + Name: goodOPCWithStatus.Status.Secrets.JWKS.Name, Namespace: namespace, Labels: map[string]string{ "myLabelKey1": "myLabelValue1", diff --git a/internal/mocks/mocksecrethelper/mocksecrethelper.go b/internal/mocks/mocksecrethelper/mocksecrethelper.go index d191d2d9..68b6cfbd 100644 --- a/internal/mocks/mocksecrethelper/mocksecrethelper.go +++ b/internal/mocks/mocksecrethelper/mocksecrethelper.go @@ -81,14 +81,16 @@ func (mr *MockSecretHelperMockRecorder) NamePrefix() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NamePrefix", reflect.TypeOf((*MockSecretHelper)(nil).NamePrefix)) } -// Notify mocks base method -func (m *MockSecretHelper) Notify(arg0 *v1alpha1.OIDCProvider, arg1 *v1.Secret) { +// ObserveActiveSecretAndUpdateParentOIDCProvider mocks base method +func (m *MockSecretHelper) ObserveActiveSecretAndUpdateParentOIDCProvider(arg0 *v1alpha1.OIDCProvider, arg1 *v1.Secret) *v1alpha1.OIDCProvider { m.ctrl.T.Helper() - m.ctrl.Call(m, "Notify", arg0, arg1) + ret := m.ctrl.Call(m, "ObserveActiveSecretAndUpdateParentOIDCProvider", arg0, arg1) + ret0, _ := ret[0].(*v1alpha1.OIDCProvider) + return ret0 } -// Notify indicates an expected call of Notify -func (mr *MockSecretHelperMockRecorder) Notify(arg0, arg1 interface{}) *gomock.Call { +// ObserveActiveSecretAndUpdateParentOIDCProvider indicates an expected call of ObserveActiveSecretAndUpdateParentOIDCProvider +func (mr *MockSecretHelperMockRecorder) ObserveActiveSecretAndUpdateParentOIDCProvider(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Notify", reflect.TypeOf((*MockSecretHelper)(nil).Notify), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObserveActiveSecretAndUpdateParentOIDCProvider", reflect.TypeOf((*MockSecretHelper)(nil).ObserveActiveSecretAndUpdateParentOIDCProvider), arg0, arg1) } diff --git a/test/integration/supervisor_secrets_test.go b/test/integration/supervisor_secrets_test.go index 9c3f8c0f..038b6fac 100644 --- a/test/integration/supervisor_secrets_test.go +++ b/test/integration/supervisor_secrets_test.go @@ -45,28 +45,28 @@ func TestSupervisorSecrets(t *testing.T) { { name: "jwks", secretName: func(op *configv1alpha1.OIDCProvider) string { - return op.Status.JWKSSecret.Name + return op.Status.Secrets.JWKS.Name }, ensureValid: ensureValidJWKS, }, { name: "hmac signing secret", secretName: func(op *configv1alpha1.OIDCProvider) string { - return "pinniped-oidc-provider-hmac-key-" + string(op.UID) + return op.Status.Secrets.TokenSigningKey.Name }, ensureValid: ensureValidSymmetricKey, }, { name: "state signature secret", secretName: func(op *configv1alpha1.OIDCProvider) string { - return "pinniped-oidc-provider-upstream-state-signature-key-" + string(op.UID) + return op.Status.Secrets.StateSigningKey.Name }, ensureValid: ensureValidSymmetricKey, }, { name: "state encryption secret", secretName: func(op *configv1alpha1.OIDCProvider) string { - return "pinniped-oidc-provider-upstream-state-encryption-key-" + string(op.UID) + return op.Status.Secrets.StateEncryptionKey.Name }, ensureValid: ensureValidSymmetricKey, }, From 73209282355bce9c5edd41b34e745a29ed5b7f26 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Tue, 15 Dec 2020 09:58:23 -0500 Subject: [PATCH 34/51] Get rid of TODOs in code by punting on them We will do these later; they have been recorded in a work tracking record. Signed-off-by: Andrew Keesler --- .../supervisorconfig/generator/oidc_provider_secrets.go | 1 - .../controller/supervisorconfig/generator/supervisor_secrets.go | 1 - 2 files changed, 2 deletions(-) diff --git a/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go index aeada106..b188cc18 100644 --- a/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go +++ b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go @@ -56,7 +56,6 @@ func NewOIDCProviderSecretsController( }, // We want to be notified when a OPC's secret gets updated or deleted. When this happens, we // should get notified via the corresponding OPC key. - // TODO: de-dup me (jwks_writer.go). withInformer( secretInformer, pinnipedcontroller.SimpleFilter(isOPControllee, func(obj metav1.Object) controllerlib.Key { diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets.go b/internal/controller/supervisorconfig/generator/supervisor_secrets.go index d3155520..cc5153a5 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets.go @@ -36,7 +36,6 @@ type supervisorSecretsController struct { // NewSupervisorSecretsController instantiates a new controllerlib.Controller which will ensure existence of a generated secret. func NewSupervisorSecretsController( - // TODO: generate the name for the secret and label the secret with the UID of the owner? So that we don't have naming conflicts if the user has already created a Secret with that name. owner *appsv1.Deployment, labels map[string]string, kubeClient kubernetes.Interface, From 9d9040944aa66362e4b80727c1681dc7362429a3 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Tue, 15 Dec 2020 12:05:06 -0800 Subject: [PATCH 35/51] Secrets owned by `Deployment` have `Controller: false` - This is to prevent K8s internal Deployment controller from trying to manage these objects Signed-off-by: Andrew Keesler --- .../supervisorconfig/generator/generator.go | 13 ++- .../generator/supervisor_secrets.go | 8 +- .../generator/supervisor_secrets_test.go | 102 ++++++++++++------ 3 files changed, 91 insertions(+), 32 deletions(-) diff --git a/internal/controller/supervisorconfig/generator/generator.go b/internal/controller/supervisorconfig/generator/generator.go index 26328caa..067209da 100644 --- a/internal/controller/supervisorconfig/generator/generator.go +++ b/internal/controller/supervisorconfig/generator/generator.go @@ -64,12 +64,23 @@ func generateSecret(namespace, name string, labels map[string]string, secretData Version: appsv1.SchemeGroupVersion.Version, Kind: "Deployment", } + + blockOwnerDeletion := true + isController := false + return &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, OwnerReferences: []metav1.OwnerReference{ - *metav1.NewControllerRef(owner, deploymentGVK), + { + APIVersion: deploymentGVK.GroupVersion().String(), + Kind: deploymentGVK.Kind, + Name: owner.GetName(), + UID: owner.GetUID(), + BlockOwnerDeletion: &blockOwnerDeletion, + Controller: &isController, + }, }, Labels: labels, }, diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets.go b/internal/controller/supervisorconfig/generator/supervisor_secrets.go index cc5153a5..1c27c7a8 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets.go @@ -56,7 +56,13 @@ func NewSupervisorSecretsController( withInformer( secretInformer, pinnipedcontroller.SimpleFilter(func(obj metav1.Object) bool { - return metav1.IsControlledBy(obj, owner) + ownerReferences := obj.GetOwnerReferences() + for i := range obj.GetOwnerReferences() { + if ownerReferences[i].UID == owner.GetUID() { + return true + } + } + return false }, nil), controllerlib.InformerOption{}, ), diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go b/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go index 307e1f66..e452e872 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go @@ -46,6 +46,7 @@ var ( } ) +// TODO want what?? func TestSupervisorSecretsControllerFilterSecret(t *testing.T) { t.Parallel() @@ -57,39 +58,25 @@ func TestSupervisorSecretsControllerFilterSecret(t *testing.T) { wantDelete bool }{ { - name: "no owner reference", - secret: corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{}, - }, - }, - { - name: "owner reference without controller set to true", + name: "owner reference is missing", secret: corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: "some-namespace", - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: ownerGVK.String(), - Name: "some-name", - Kind: ownerGVK.Kind, - UID: owner.GetUID(), - }, - }, }, }, }, { - name: "owner reference without correct APIVersion", + name: "owner reference with incorrect `APIVersion`", secret: corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: "some-namespace", OwnerReferences: []metav1.OwnerReference{ { - Name: "some-name", - Kind: ownerGVK.Kind, - Controller: boolPtr(true), - UID: owner.GetUID(), - }}, + Name: owner.GetName(), + Kind: ownerGVK.Kind, + UID: owner.GetUID(), + }, + }, }, }, wantAdd: true, @@ -97,16 +84,15 @@ func TestSupervisorSecretsControllerFilterSecret(t *testing.T) { wantDelete: true, }, { - name: "owner reference without correct Kind", + name: "owner reference with incorrect `Kind`", secret: corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: "some-namespace", OwnerReferences: []metav1.OwnerReference{ { APIVersion: ownerGVK.String(), - Name: "some-name", + Name: owner.GetName(), Kind: "IncorrectKind", - Controller: boolPtr(true), UID: owner.GetUID(), }, }, @@ -117,7 +103,7 @@ func TestSupervisorSecretsControllerFilterSecret(t *testing.T) { wantDelete: true, }, { - name: "correct owner reference", + name: "owner reference with `Controller`: true", secret: corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: "some-namespace", @@ -131,7 +117,42 @@ func TestSupervisorSecretsControllerFilterSecret(t *testing.T) { wantDelete: true, }, { - name: "multiple owner references", + name: "expected owner reference with incorrect `UID`", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: ownerGVK.String(), + Name: owner.GetName(), + Kind: ownerGVK.Kind, + UID: "DOES_NOT_MATCH", + }, + }, + }, + }, + }, + { + name: "expected owner reference - where `Controller`: false", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: ownerGVK.String(), + Name: owner.GetName(), + Kind: ownerGVK.Kind, + UID: owner.GetUID(), + }, + }, + }, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "multiple owner references (expected owner reference, and one more)", secret: corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: "some-namespace", @@ -139,7 +160,12 @@ func TestSupervisorSecretsControllerFilterSecret(t *testing.T) { { Kind: "UnrelatedKind", }, - *metav1.NewControllerRef(owner, ownerGVK), + { + APIVersion: ownerGVK.String(), + Name: owner.GetName(), + Kind: ownerGVK.Kind, + UID: owner.GetUID(), + }, }, }, }, @@ -215,12 +241,21 @@ func TestSupervisorSecretsControllerSync(t *testing.T) { generatedSymmetricKey = []byte("some-neato-32-byte-generated-key") otherGeneratedSymmetricKey = []byte("some-funio-32-byte-generated-key") - generatedSecret = &corev1.Secret{ + blockOwnerDeletion = true + isController = false + generatedSecret = &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: generatedSecretName, Namespace: generatedSecretNamespace, OwnerReferences: []metav1.OwnerReference{ - *metav1.NewControllerRef(owner, ownerGVK), + { + APIVersion: ownerGVK.GroupVersion().String(), + Kind: ownerGVK.Kind, + Name: owner.GetName(), + UID: owner.GetUID(), + BlockOwnerDeletion: &blockOwnerDeletion, + Controller: &isController, + }, }, Labels: labels, }, @@ -235,7 +270,14 @@ func TestSupervisorSecretsControllerSync(t *testing.T) { Name: generatedSecretName, Namespace: generatedSecretNamespace, OwnerReferences: []metav1.OwnerReference{ - *metav1.NewControllerRef(owner, ownerGVK), + { + APIVersion: ownerGVK.GroupVersion().String(), + Kind: ownerGVK.Kind, + Name: owner.GetName(), + UID: owner.GetUID(), + BlockOwnerDeletion: &blockOwnerDeletion, + Controller: &isController, + }, }, Labels: labels, }, From 35bb76ea82c3ecc8ff4da6ca4e95560ce61eb572 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Tue, 15 Dec 2020 15:55:14 -0500 Subject: [PATCH 36/51] Ensure labels are set correct on generated Supervisor secret Signed-off-by: Andrew Keesler --- .../supervisorconfig/generator/generator.go | 8 +++++- .../generator/supervisor_secrets.go | 7 +++-- .../generator/supervisor_secrets_test.go | 26 ++++++++++++++++++- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/internal/controller/supervisorconfig/generator/generator.go b/internal/controller/supervisorconfig/generator/generator.go index 067209da..fe711698 100644 --- a/internal/controller/supervisorconfig/generator/generator.go +++ b/internal/controller/supervisorconfig/generator/generator.go @@ -26,7 +26,7 @@ func generateSymmetricKey() ([]byte, error) { return b, nil } -func isValid(secret *corev1.Secret) bool { +func isValid(secret *corev1.Secret, labels map[string]string) bool { if secret.Type != symmetricSecretType { return false } @@ -39,6 +39,12 @@ func isValid(secret *corev1.Secret) bool { return false } + for key, value := range labels { + if secret.Labels[key] != value { + return false + } + } + return true } diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets.go b/internal/controller/supervisorconfig/generator/supervisor_secrets.go index 1c27c7a8..9f5ca1c4 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets.go @@ -81,7 +81,7 @@ func (c *supervisorSecretsController) Sync(ctx controllerlib.Context) error { return fmt.Errorf("failed to list secret %s/%s: %w", ctx.Key.Namespace, ctx.Key.Name, err) } - secretNeedsUpdate := isNotFound || !isValid(secret) + secretNeedsUpdate := isNotFound || !isValid(secret, c.labels) if !secretNeedsUpdate { plog.Debug("secret is up to date", "secret", klog.KObj(secret)) c.setCacheFunc(secret.Data[symmetricSecretDataKey]) @@ -128,13 +128,16 @@ func (c *supervisorSecretsController) updateSecret(ctx context.Context, newSecre return nil } - if isValid(currentSecret) { + if isValid(currentSecret, c.labels) { *newSecret = currentSecret return nil } currentSecret.Type = (*newSecret).Type currentSecret.Data = (*newSecret).Data + for key, value := range c.labels { + currentSecret.Labels[key] = value + } _, err = secrets.Update(ctx, currentSecret, metav1.UpdateOptions{}) return err diff --git a/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go b/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go index e452e872..2522fe7d 100644 --- a/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go +++ b/internal/controller/supervisorconfig/generator/supervisor_secrets_test.go @@ -46,7 +46,6 @@ var ( } ) -// TODO want what?? func TestSupervisorSecretsControllerFilterSecret(t *testing.T) { t.Parallel() @@ -288,6 +287,9 @@ func TestSupervisorSecretsControllerSync(t *testing.T) { } ) + // Add an extra label to make sure we don't overwrite existing labels on a Secret. + generatedSecret.Labels["extra-label-key"] = "extra-label-value" + once := sync.Once{} tests := []struct { @@ -429,6 +431,28 @@ func TestSupervisorSecretsControllerSync(t *testing.T) { }, wantCallbackSecret: otherGeneratedSymmetricKey, }, + { + name: "upon updating we discover that a secret with missing labels exists", + storedSecret: func(secret **corev1.Secret) { + delete((*secret).Labels, "some-label-key-1") + }, + wantActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), + kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecret), + }, + wantCallbackSecret: generatedSymmetricKey, + }, + { + name: "upon updating we discover that a secret with incorrect labels exists", + storedSecret: func(secret **corev1.Secret) { + (*secret).Labels["some-label-key-1"] = "incorrect" + }, + wantActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName), + kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecret), + }, + wantCallbackSecret: generatedSymmetricKey, + }, { name: "upon updating we discover that the secret has been deleted", storedSecret: func(secret **corev1.Secret) { From 0bd428e45d16629bdcd38ed7f5e409312e197f62 Mon Sep 17 00:00:00 2001 From: Aram Price Date: Tue, 15 Dec 2020 16:49:24 -0500 Subject: [PATCH 37/51] test/integration: more logging to track down flakes Signed-off-by: Andrew Keesler --- test/integration/supervisor_discovery_test.go | 3 +++ test/integration/supervisor_login_test.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index 5e12fc12..996c3bfe 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -599,6 +599,9 @@ func requireStatus(t *testing.T, client pinnipedclientset.Interface, ns, name st var err error assert.Eventually(t, func() bool { opc, err = client.ConfigV1alpha1().OIDCProviders(ns).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + t.Logf("error trying to get OIDCProvider: %s", err.Error()) + } return err == nil && opc.Status.Status == status }, 10*time.Second, 200*time.Millisecond) require.NoError(t, err) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 6fa7875f..10b50acd 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -169,6 +169,9 @@ func TestSupervisorLogin(t *testing.T) { var tokenResponse *oauth2.Token assert.Eventually(t, func() bool { tokenResponse, err = downstreamOAuth2Config.Exchange(oidcHTTPClientContext, authcode, pkceParam.Verifier()) + if err != nil { + t.Logf("error trying to exchange auth code (%s), trying again", err.Error()) + } return err == nil }, time.Second*5, time.Second*1) require.NoError(t, err) From 0758ecfea8ca91cb59b011471c96f5d30c1eec08 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Tue, 15 Dec 2020 15:46:55 -0800 Subject: [PATCH 38/51] Tests wait for OIDCProvider secrets to be set Signed-off-by: aram price --- test/library/client.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/library/client.go b/test/library/client.go index 0f622b44..c68c0872 100644 --- a/test/library/client.go +++ b/test/library/client.go @@ -285,6 +285,22 @@ func CreateTestOIDCProvider(ctx context.Context, t *testing.T, issuer string, ce }, 60*time.Second, 1*time.Second, "expected the OIDCProvider to have status %q", expectStatus) require.Equal(t, expectStatus, result.Status.Status) + // If the expected status is success, also wait for the secrets to be created. + if expectStatus == configv1alpha1.SuccessOIDCProviderStatusCondition { + assert.Eventually(t, func() bool { + var err error + result, err = opcs.Get(ctx, opc.Name, metav1.GetOptions{}) + require.NoError(t, err) + return result.Status.Secrets.JWKS.Name != "" && + result.Status.Secrets.TokenSigningKey.Name != "" && + result.Status.Secrets.StateSigningKey.Name != "" && + result.Status.Secrets.StateEncryptionKey.Name != "" + }, 60*time.Second, 1*time.Second, "expected the OIDCProvider to have secrets populated") + require.NotEmpty(t, result.Status.Secrets.JWKS.Name) + require.NotEmpty(t, result.Status.Secrets.TokenSigningKey.Name) + require.NotEmpty(t, result.Status.Secrets.StateSigningKey.Name) + require.NotEmpty(t, result.Status.Secrets.StateEncryptionKey.Name) + } return opc } From 78df80f12888095a74376efb1eb810bb0a71ff45 Mon Sep 17 00:00:00 2001 From: aram price Date: Tue, 15 Dec 2020 18:26:27 -0800 Subject: [PATCH 39/51] Tests ensure OIDCProvider secrets exist ... whenever one is successfully created. --- test/library/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/library/client.go b/test/library/client.go index c68c0872..a6e5fa57 100644 --- a/test/library/client.go +++ b/test/library/client.go @@ -285,8 +285,8 @@ func CreateTestOIDCProvider(ctx context.Context, t *testing.T, issuer string, ce }, 60*time.Second, 1*time.Second, "expected the OIDCProvider to have status %q", expectStatus) require.Equal(t, expectStatus, result.Status.Status) - // If the expected status is success, also wait for the secrets to be created. - if expectStatus == configv1alpha1.SuccessOIDCProviderStatusCondition { + // If the OIDCProvider was successfully created, ensure all secrets are present before continuing + if result.Status.Status == configv1alpha1.SuccessOIDCProviderStatusCondition { assert.Eventually(t, func() bool { var err error result, err = opcs.Get(ctx, opc.Name, metav1.GetOptions{}) From 404ff93102a2813010dfac641c6d6bdd34e4dd39 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 15 Dec 2020 21:52:07 -0600 Subject: [PATCH 40/51] Fix documentation comment for the UpstreamOIDCProvider's spec.client.secretName type. The value is correctly validated as `secrets.pinniped.dev/oidc-client` elsewhere, only this comment was wrong. Signed-off-by: Matt Moyer --- apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go.tmpl | 2 +- .../idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml | 2 +- generated/1.17/README.adoc | 2 +- .../apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go | 2 +- .../crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml | 2 +- generated/1.18/README.adoc | 2 +- .../apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go | 2 +- .../crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml | 2 +- generated/1.19/README.adoc | 2 +- .../apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go | 2 +- .../crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go.tmpl index 9be04701..09f74c7c 100644 --- a/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go.tmpl @@ -62,7 +62,7 @@ type OIDCClaims struct { type OIDCClient struct { // SecretName contains the name of a namespace-local Secret object that provides the clientID and // clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient - // struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc" with keys + // struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys // "clientID" and "clientSecret". SecretName string `json:"secretName"` } diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml index 780fe6fe..bd239a6f 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml @@ -86,7 +86,7 @@ spec: description: SecretName contains the name of a namespace-local Secret object that provides the clientID and clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient - struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc" + struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys "clientID" and "clientSecret". type: string required: diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 0ccdd1df..1303b2a4 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -442,7 +442,7 @@ OIDCClient contains information about an OIDC client (e.g., client ID and client [cols="25a,75a", options="header"] |=== | Field | Description -| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the clientID and clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc" with keys "clientID" and "clientSecret". +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the clientID and clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys "clientID" and "clientSecret". |=== diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go index 9be04701..09f74c7c 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go @@ -62,7 +62,7 @@ type OIDCClaims struct { type OIDCClient struct { // SecretName contains the name of a namespace-local Secret object that provides the clientID and // clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient - // struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc" with keys + // struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys // "clientID" and "clientSecret". SecretName string `json:"secretName"` } diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml index 780fe6fe..bd239a6f 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml @@ -86,7 +86,7 @@ spec: description: SecretName contains the name of a namespace-local Secret object that provides the clientID and clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient - struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc" + struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys "clientID" and "clientSecret". type: string required: diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index 97042b25..7c3dda8f 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -442,7 +442,7 @@ OIDCClient contains information about an OIDC client (e.g., client ID and client [cols="25a,75a", options="header"] |=== | Field | Description -| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the clientID and clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc" with keys "clientID" and "clientSecret". +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the clientID and clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys "clientID" and "clientSecret". |=== diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go index 9be04701..09f74c7c 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go @@ -62,7 +62,7 @@ type OIDCClaims struct { type OIDCClient struct { // SecretName contains the name of a namespace-local Secret object that provides the clientID and // clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient - // struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc" with keys + // struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys // "clientID" and "clientSecret". SecretName string `json:"secretName"` } diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml index 780fe6fe..bd239a6f 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml @@ -86,7 +86,7 @@ spec: description: SecretName contains the name of a namespace-local Secret object that provides the clientID and clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient - struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc" + struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys "clientID" and "clientSecret". type: string required: diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index edda33b8..d4d0c8b1 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -442,7 +442,7 @@ OIDCClient contains information about an OIDC client (e.g., client ID and client [cols="25a,75a", options="header"] |=== | Field | Description -| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the clientID and clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc" with keys "clientID" and "clientSecret". +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the clientID and clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys "clientID" and "clientSecret". |=== diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go index 9be04701..09f74c7c 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go @@ -62,7 +62,7 @@ type OIDCClaims struct { type OIDCClient struct { // SecretName contains the name of a namespace-local Secret object that provides the clientID and // clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient - // struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc" with keys + // struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys // "clientID" and "clientSecret". SecretName string `json:"secretName"` } diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml index 780fe6fe..bd239a6f 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_upstreamoidcproviders.yaml @@ -86,7 +86,7 @@ spec: description: SecretName contains the name of a namespace-local Secret object that provides the clientID and clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient - struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc" + struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys "clientID" and "clientSecret". type: string required: From 5bdbfe1bc605464d97257266fb23b0091f739de8 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Wed, 16 Dec 2020 08:23:54 -0500 Subject: [PATCH 41/51] test/integration: more verbosity to try to track down flakes... Signed-off-by: Andrew Keesler --- test/integration/e2e_test.go | 17 +++++++++++++++-- test/integration/supervisor_discovery_test.go | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 1bf58c3f..e77f1a6e 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -35,6 +35,11 @@ import ( // TestE2EFullIntegration tests a full integration scenario that combines the supervisor, concierge, and CLI. func TestE2EFullIntegration(t *testing.T) { env := library.IntegrationEnv(t).WithCapability(library.ClusterSigningKeyIsAvailable) + + // If anything in this test crashes, dump out the supervisor and proxy pod logs. + defer library.DumpLogs(t, env.SupervisorNamespace, "") + defer library.DumpLogs(t, "dex", "app=proxy") + ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Minute) defer cancelFunc() @@ -160,7 +165,15 @@ func TestE2EFullIntegration(t *testing.T) { t.Cleanup(func() { err := kubectlCmd.Wait() t.Logf("kubectl subprocess exited with code %d", kubectlCmd.ProcessState.ExitCode()) - require.NoErrorf(t, err, "kubectl process did not exit cleanly") + stdout, stdoutErr := ioutil.ReadAll(stdoutPipe) + if stdoutErr != nil { + stdout = []byte("") + } + stderr, stderrErr := ioutil.ReadAll(stderrPipe) + if stderrErr != nil { + stderr = []byte("") + } + require.NoErrorf(t, err, "kubectl process did not exit cleanly, stdout/stderr: %q/%q", string(stdout), string(stderr)) }) // Start a background goroutine to read stderr from the CLI and parse out the login URL. @@ -244,7 +257,7 @@ func TestE2EFullIntegration(t *testing.T) { require.Fail(t, "timed out waiting for kubectl output") case kubectlOutput = <-kubectlOutputChan: } - require.Greaterf(t, len(strings.Split(kubectlOutput, "\n")), 2, "expected some namespaces to be returned") + require.Greaterf(t, len(strings.Split(kubectlOutput, "\n")), 2, "expected some namespaces to be returned, got %q", kubectlOutput) t.Logf("first kubectl command took %s", time.Since(start).String()) // Run kubectl again, which should work with no browser interaction. diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index 996c3bfe..fa378245 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -592,7 +592,7 @@ func requireDelete(t *testing.T, client pinnipedclientset.Interface, ns, name st func requireStatus(t *testing.T, client pinnipedclientset.Interface, ns, name string, status v1alpha1.OIDCProviderStatusCondition) { t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() var opc *v1alpha1.OIDCProvider From fec80113c7ceccfdc5dd8494cc0d1afb163af5c2 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Wed, 16 Dec 2020 08:26:44 -0500 Subject: [PATCH 42/51] Revert "Retry a couple of times if we fail to get a token from the Supervisor" This reverts commit be4e34d0c06cc7c8a27014c76e62e147e723eaf5. Roll back this change that was supposed to make the test more robust. If we retry multiple token exchanges with the same auth code, of course we are going to get failures on the second try onwards because the auth code was invalidated on the first try. Signed-off-by: Andrew Keesler --- test/integration/supervisor_login_test.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 10b50acd..ed4ca1eb 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -165,15 +165,8 @@ func TestSupervisorLogin(t *testing.T) { authcode := callback.URL.Query().Get("code") require.NotEmpty(t, authcode) - // Call the token endpoint to get tokens. Give the Supervisor a couple of seconds to wire up its signing key. - var tokenResponse *oauth2.Token - assert.Eventually(t, func() bool { - tokenResponse, err = downstreamOAuth2Config.Exchange(oidcHTTPClientContext, authcode, pkceParam.Verifier()) - if err != nil { - t.Logf("error trying to exchange auth code (%s), trying again", err.Error()) - } - return err == nil - }, time.Second*5, time.Second*1) + // Call the token endpoint to get tokens. + tokenResponse, err := downstreamOAuth2Config.Exchange(oidcHTTPClientContext, authcode, pkceParam.Verifier()) require.NoError(t, err) expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat"} From a33dace80bfc4c555e7bc3bc0a4425a07407001d Mon Sep 17 00:00:00 2001 From: Aram Price Date: Wed, 16 Dec 2020 13:31:54 -0500 Subject: [PATCH 43/51] Upgrade golang (1.15.5 -> 1.15.6) Signed-off-by: Andrew Keesler --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index be74db5f..73c50799 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # Copyright 2020 the Pinniped contributors. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -FROM golang:1.15.5 as build-env +FROM golang:1.15.6 as build-env WORKDIR /work # Get dependencies first so they can be cached as a layer From 602f3c59babdb7090771c4b61fe8d9db3cd1eed1 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 15 Dec 2020 21:34:34 -0600 Subject: [PATCH 44/51] Fix a regression in securityheader package. The bug itself has to do with when headers are streamed to the client. Once a wrapped handler has sent any bytes to the `http.ResponseWriter`, the value of the map returned from `w.Header()` no longer matters for the response. The fix is fairly trivial, which is to add those response headers before invoking the wrapped handler. The existing unit test didn't catch this due to limitations in `httptest.NewRecorder()`. It is now replaced with a new test that runs a full HTTP test server, which catches the previous bug. Signed-off-by: Matt Moyer --- .../httputil/securityheader/securityheader.go | 3 +- .../securityheader/securityheader_test.go | 37 +++++++++++++++---- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/internal/httputil/securityheader/securityheader.go b/internal/httputil/securityheader/securityheader.go index 42cf1e2f..2def7ede 100644 --- a/internal/httputil/securityheader/securityheader.go +++ b/internal/httputil/securityheader/securityheader.go @@ -9,7 +9,6 @@ import "net/http" // Wrap the provided http.Handler so it sets appropriate security-related response headers. func Wrap(wrapped http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - wrapped.ServeHTTP(w, r) h := w.Header() h.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'") h.Set("X-Frame-Options", "DENY") @@ -26,5 +25,7 @@ func Wrap(wrapped http.Handler) http.Handler { h.Set("Pragma", "no-cache") h.Set("Expires", "0") + + wrapped.ServeHTTP(w, r) }) } diff --git a/internal/httputil/securityheader/securityheader_test.go b/internal/httputil/securityheader/securityheader_test.go index 715e7cd5..e8772f51 100644 --- a/internal/httputil/securityheader/securityheader_test.go +++ b/internal/httputil/securityheader/securityheader_test.go @@ -4,22 +4,40 @@ package securityheader import ( + "context" + "io/ioutil" "net/http" "net/http/httptest" "testing" + "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestWrap(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + testServer := httptest.NewServer(Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Test-Header", "test value") _, _ = w.Write([]byte("hello world")) - }) - rec := httptest.NewRecorder() - Wrap(handler).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil)) - require.Equal(t, http.StatusOK, rec.Code) - require.Equal(t, "hello world", rec.Body.String()) - require.EqualValues(t, http.Header{ + }))) + t.Cleanup(testServer.Close) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, testServer.URL, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + + respBody, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "hello world", string(respBody)) + + expected := http.Header{ + "X-Test-Header": []string{"test value"}, "Content-Security-Policy": []string{"default-src 'none'; frame-ancestors 'none'"}, "Content-Type": []string{"text/plain; charset=utf-8"}, "Referrer-Policy": []string{"no-referrer"}, @@ -30,5 +48,8 @@ func TestWrap(t *testing.T) { "Cache-Control": []string{"no-cache", "no-store", "max-age=0", "must-revalidate"}, "Pragma": []string{"no-cache"}, "Expires": []string{"0"}, - }, rec.Header()) + } + for key, values := range expected { + assert.Equalf(t, values, resp.Header.Values(key), "unexpected values for header %s", key) + } } From 74e52187a3a0bc0579ffc259191ffa9c589c7a02 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 15 Dec 2020 21:38:55 -0600 Subject: [PATCH 45/51] Simplify securityheader package by merging header fields. From RFC2616 (https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2): > It MUST be possible to combine the multiple header fields into one "field-name: field-value" pair, > without changing the semantics of the message, by appending each subsequent field-value to the first, > each separated by a comma. This was correct before, but this simplifes a bit and shaves off a few bytes from the response. Signed-off-by: Matt Moyer --- internal/httputil/securityheader/securityheader.go | 9 +-------- internal/httputil/securityheader/securityheader_test.go | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/internal/httputil/securityheader/securityheader.go b/internal/httputil/securityheader/securityheader.go index 2def7ede..2bb3af12 100644 --- a/internal/httputil/securityheader/securityheader.go +++ b/internal/httputil/securityheader/securityheader.go @@ -16,16 +16,9 @@ func Wrap(wrapped http.Handler) http.Handler { h.Set("X-Content-Type-Options", "nosniff") h.Set("Referrer-Policy", "no-referrer") h.Set("X-DNS-Prefetch-Control", "off") - - // first overwrite existing Cache-Control header with Set, then append more headers with Add - h.Set("Cache-Control", "no-cache") - h.Add("Cache-Control", "no-store") - h.Add("Cache-Control", "max-age=0") - h.Add("Cache-Control", "must-revalidate") - + h.Set("Cache-Control", "no-cache,no-store,max-age=0,must-revalidate") h.Set("Pragma", "no-cache") h.Set("Expires", "0") - wrapped.ServeHTTP(w, r) }) } diff --git a/internal/httputil/securityheader/securityheader_test.go b/internal/httputil/securityheader/securityheader_test.go index e8772f51..a0688c1a 100644 --- a/internal/httputil/securityheader/securityheader_test.go +++ b/internal/httputil/securityheader/securityheader_test.go @@ -45,7 +45,7 @@ func TestWrap(t *testing.T) { "X-Frame-Options": []string{"DENY"}, "X-Xss-Protection": []string{"1; mode=block"}, "X-Dns-Prefetch-Control": []string{"off"}, - "Cache-Control": []string{"no-cache", "no-store", "max-age=0", "must-revalidate"}, + "Cache-Control": []string{"no-cache,no-store,max-age=0,must-revalidate"}, "Pragma": []string{"no-cache"}, "Expires": []string{"0"}, } From 24c01d3e5491cffff69fa3cdbc10762d2ca0f3b9 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 15 Dec 2020 21:42:11 -0600 Subject: [PATCH 46/51] Add an integration test to verify security headers on the supervisor authorize endpoint. It would be great to do this for the supervisor's callback endpoint as well, but it's difficult to get at those since the request happens inside the spawned browser. Signed-off-by: Matt Moyer --- test/integration/supervisor_login_test.go | 51 +++++++++++++++++------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 6fa7875f..6fc03a7d 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -57,19 +57,25 @@ func TestSupervisorLogin(t *testing.T) { require.NoError(t, err) // Create an HTTP client that can reach the downstream discovery endpoint using the CA certs. - httpClient := &http.Client{Transport: &http.Transport{ - TLSClientConfig: &tls.Config{RootCAs: ca.Pool()}, - Proxy: func(req *http.Request) (*url.URL, error) { - if env.Proxy == "" { - t.Logf("passing request for %s with no proxy", req.URL) - return nil, nil - } - proxyURL, err := url.Parse(env.Proxy) - require.NoError(t, err) - t.Logf("passing request for %s through proxy %s", req.URL, proxyURL.String()) - return proxyURL, nil + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{RootCAs: ca.Pool()}, + Proxy: func(req *http.Request) (*url.URL, error) { + if env.Proxy == "" { + t.Logf("passing request for %s with no proxy", req.URL) + return nil, nil + } + proxyURL, err := url.Parse(env.Proxy) + require.NoError(t, err) + t.Logf("passing request for %s through proxy %s", req.URL, proxyURL.String()) + return proxyURL, nil + }, }, - }} + // Don't follow redirects automatically. + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } oidcHTTPClientContext := coreosoidc.ClientContext(ctx, httpClient) // Use the CA to issue a TLS server cert. @@ -144,6 +150,14 @@ func TestSupervisorLogin(t *testing.T) { pkceParam.Method(), ) + // Make the authorize request one "manually" so we can check its response headers. + authorizeRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, downstreamAuthorizeURL, nil) + require.NoError(t, err) + authorizeResp, err := httpClient.Do(authorizeRequest) + require.NoError(t, err) + require.NoError(t, authorizeResp.Body.Close()) + expectSecurityHeaders(t, authorizeResp) + // Open the web browser and navigate to the downstream authorize URL. page := browsertest.Open(t) t.Logf("opening browser to downstream authorize URL %s", library.MaskTokens(downstreamAuthorizeURL)) @@ -306,3 +320,16 @@ func doTokenExchange(t *testing.T, config *oauth2.Config, tokenResponse *oauth2. require.NoError(t, err) t.Logf("exchanged token claims:\n%s", string(indentedClaims)) } + +func expectSecurityHeaders(t *testing.T, response *http.Response) { + h := response.Header + assert.Equal(t, "default-src 'none'; frame-ancestors 'none'", h.Get("Content-Security-Policy")) + assert.Equal(t, "DENY", h.Get("X-Frame-Options")) + assert.Equal(t, "1; mode=block", h.Get("X-XSS-Protection")) + assert.Equal(t, "nosniff", h.Get("X-Content-Type-Options")) + assert.Equal(t, "no-referrer", h.Get("Referrer-Policy")) + assert.Equal(t, "off", h.Get("X-DNS-Prefetch-Control")) + assert.Equal(t, "no-cache,no-store,max-age=0,must-revalidate", h.Get("Cache-Control")) + assert.Equal(t, "no-cache", h.Get("Pragma")) + assert.Equal(t, "0", h.Get("Expires")) +} From 3948bb76d870d6cb07d72ca10ccceb153255dece Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Wed, 16 Dec 2020 13:15:38 -0600 Subject: [PATCH 47/51] Be more lax in some of our test assertions. Fosite overrides the `Cache-Control` header we set, which is basically fine even though it's not exactly what we want. Signed-off-by: Matt Moyer --- internal/testutil/assertions.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/testutil/assertions.go b/internal/testutil/assertions.go index b0c3018d..54fc8563 100644 --- a/internal/testutil/assertions.go +++ b/internal/testutil/assertions.go @@ -61,7 +61,9 @@ func RequireSecurityHeaders(t *testing.T, response *httptest.ResponseRecorder) { require.Equal(t, "nosniff", response.Header().Get("X-Content-Type-Options")) require.Equal(t, "no-referrer", response.Header().Get("Referrer-Policy")) require.Equal(t, "off", response.Header().Get("X-DNS-Prefetch-Control")) - require.ElementsMatch(t, []string{"no-cache", "no-store", "max-age=0", "must-revalidate"}, response.Header().Values("Cache-Control")) require.Equal(t, "no-cache", response.Header().Get("Pragma")) require.Equal(t, "0", response.Header().Get("Expires")) + + // This check is more relaxed since Fosite can override the base header we set. + require.Contains(t, response.Header().Get("Cache-Control"), "no-store") } From 01b6bf7850d580fcc44d1ef9c49a43388866b6d2 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 15 Dec 2020 22:11:49 -0600 Subject: [PATCH 48/51] Tweak timeouts in oidcclient package. - The overall timeout for logins is increased to 90 minutes. - The timeout for token refresh is increased from 30 seconds to 60 seconds to be a bit more tolerant of extremely slow networks. - A new, matching timeout of 60 seconds has been added for the OIDC discovery, auth code exchange, and RFC8693 token exchange operations. The new code uses the `http.Client.Timeout` field rather than managing contexts on individual requests. This is easier because the OIDC package stores a context at creation time and tries to use it later when performing key refresh operations. Signed-off-by: Matt Moyer --- pkg/oidcclient/login.go | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index 4d92a452..5ae1bf0c 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -37,9 +37,13 @@ const ( // API operation. minIDTokenValidity = 10 * time.Minute - // refreshTimeout is the amount of time allotted for OAuth2 refresh operations. Since these don't involve any - // user interaction, they should always be roughly as fast as network latency. - refreshTimeout = 30 * time.Second + // httpRequestTimeout is the timeout for operations that involve one (or a few) non-interactive HTTPS requests. + // Since these don't involve any user interaction, they should always be roughly as fast as network latency. + httpRequestTimeout = 60 * time.Second + + // overallTimeout is the overall time that a login is allowed to take. This includes several user interactions, so + // we set this to be relatively long. + overallTimeout = 90 * time.Minute ) type handlerState struct { @@ -198,8 +202,13 @@ func Login(issuer string, clientID string, opts ...Option) (*oidctypes.Token, er } } + // Copy the configured HTTP client to set a request timeout (the Go default client has no timeout configured). + httpClientWithTimeout := *h.httpClient + httpClientWithTimeout.Timeout = httpRequestTimeout + h.httpClient = &httpClientWithTimeout + // Always set a long, but non-infinite timeout for this operation. - ctx, cancel := context.WithTimeout(h.ctx, 10*time.Minute) + ctx, cancel := context.WithTimeout(h.ctx, overallTimeout) defer cancel() ctx = oidc.ClientContext(ctx, h.httpClient) h.ctx = ctx @@ -404,8 +413,6 @@ func (h *handlerState) tokenExchangeRFC8693(baseToken *oidctypes.Token) (*oidcty } func (h *handlerState) handleRefresh(ctx context.Context, refreshToken *oidctypes.RefreshToken) (*oidctypes.Token, error) { - ctx, cancel := context.WithTimeout(ctx, refreshTimeout) - defer cancel() refreshSource := h.oauth2Config.TokenSource(ctx, &oauth2.Token{RefreshToken: refreshToken.Token}) refreshed, err := refreshSource.Token() @@ -473,7 +480,7 @@ func (h *handlerState) serve(listener net.Listener) func() { return func() { // Gracefully shut down the server, allowing up to 5 seconds for // clients to receive any in-flight responses. - shutdownCtx, cancel := context.WithTimeout(h.ctx, 1*time.Second) + shutdownCtx, cancel := context.WithTimeout(h.ctx, 5*time.Second) _ = srv.Shutdown(shutdownCtx) cancel() } From 8527c363bb46fffa4ee636ef0047b7db1e5463fb Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 15 Dec 2020 21:59:57 -0600 Subject: [PATCH 49/51] Rename the "pinniped.sts.unrestricted" scope to "pinniped:request-audience". This is a bit more clear. We're changing this now because it is a non-backwards-compatible change that we can make now since none of this RFC8693 token exchange stuff has been released yet. There is also a small typo fix in some flag usages (s/RF8693/RFC8693/) Signed-off-by: Matt Moyer --- cmd/pinniped/cmd/kubeconfig.go | 4 +-- cmd/pinniped/cmd/kubeconfig_test.go | 8 ++--- cmd/pinniped/cmd/login_oidc.go | 4 +-- cmd/pinniped/cmd/login_oidc_test.go | 4 +-- internal/oidc/auth/auth_handler.go | 4 +-- internal/oidc/callback/callback_handler.go | 4 +-- internal/oidc/nullstorage_test.go | 2 +- internal/oidc/oidc.go | 2 +- internal/oidc/token/token_handler_test.go | 34 +++++++++++----------- internal/oidc/token_exchange.go | 4 +-- pkg/oidcclient/login.go | 2 +- test/integration/supervisor_login_test.go | 4 +-- 12 files changed, 38 insertions(+), 38 deletions(-) diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index 5bf8efe8..913e7ca4 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -102,12 +102,12 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command { f.StringVar(&flags.oidc.issuer, "oidc-issuer", "", "OpenID Connect issuer URL (default: autodiscover)") f.StringVar(&flags.oidc.clientID, "oidc-client-id", "pinniped-cli", "OpenID Connect client ID (default: autodiscover)") f.Uint16Var(&flags.oidc.listenPort, "oidc-listen-port", 0, "TCP port for localhost listener (authorization code flow only)") - f.StringSliceVar(&flags.oidc.scopes, "oidc-scopes", []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID, "pinniped.sts.unrestricted"}, "OpenID Connect scopes to request during login") + f.StringSliceVar(&flags.oidc.scopes, "oidc-scopes", []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID, "pinniped:request-audience"}, "OpenID Connect scopes to request during login") f.BoolVar(&flags.oidc.skipBrowser, "oidc-skip-browser", false, "During OpenID Connect login, skip opening the browser (just print the URL)") f.StringVar(&flags.oidc.sessionCachePath, "oidc-session-cache", "", "Path to OpenID Connect session cache file") f.StringSliceVar(&flags.oidc.caBundlePaths, "oidc-ca-bundle", nil, "Path to TLS certificate authority bundle (PEM format, optional, can be repeated)") f.BoolVar(&flags.oidc.debugSessionCache, "oidc-debug-session-cache", false, "Print debug logs related to the OpenID Connect session cache") - f.StringVar(&flags.oidc.requestAudience, "oidc-request-audience", "", "Request a token with an alternate audience using RF8693 token exchange") + f.StringVar(&flags.oidc.requestAudience, "oidc-request-audience", "", "Request a token with an alternate audience using RFC8693 token exchange") f.StringVar(&flags.kubeconfigPath, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to kubeconfig file") f.StringVar(&flags.kubeconfigContextOverride, "kubeconfig-context", "", "Kubeconfig context name (default: current active context)") diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index 739df951..e0fc8480 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -68,8 +68,8 @@ func TestGetKubeconfig(t *testing.T) { --oidc-client-id string OpenID Connect client ID (default: autodiscover) (default "pinniped-cli") --oidc-issuer string OpenID Connect issuer URL (default: autodiscover) --oidc-listen-port uint16 TCP port for localhost listener (authorization code flow only) - --oidc-request-audience string Request a token with an alternate audience using RF8693 token exchange - --oidc-scopes strings OpenID Connect scopes to request during login (default [offline_access,openid,pinniped.sts.unrestricted]) + --oidc-request-audience string Request a token with an alternate audience using RFC8693 token exchange + --oidc-scopes strings OpenID Connect scopes to request during login (default [offline_access,openid,pinniped:request-audience]) --oidc-session-cache string Path to OpenID Connect session cache file --oidc-skip-browser During OpenID Connect login, skip opening the browser (just print the URL) --static-token string Instead of doing an OIDC-based login, specify a static token @@ -415,7 +415,7 @@ func TestGetKubeconfig(t *testing.T) { - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== - --issuer=https://example.com/issuer - --client-id=pinniped-cli - - --scopes=offline_access,openid,pinniped.sts.unrestricted + - --scopes=offline_access,openid,pinniped:request-audience - --ca-bundle-data=%s - --request-audience=test-audience command: '.../path/to/pinniped' @@ -472,7 +472,7 @@ func TestGetKubeconfig(t *testing.T) { - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== - --issuer=https://example.com/issuer - --client-id=pinniped-cli - - --scopes=offline_access,openid,pinniped.sts.unrestricted + - --scopes=offline_access,openid,pinniped:request-audience - --skip-browser - --listen-port=1234 - --ca-bundle-data=%s diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index e3078458..39718105 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -79,13 +79,13 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command { cmd.Flags().StringVar(&flags.issuer, "issuer", "", "OpenID Connect issuer URL") cmd.Flags().StringVar(&flags.clientID, "client-id", "pinniped-cli", "OpenID Connect client ID") cmd.Flags().Uint16Var(&flags.listenPort, "listen-port", 0, "TCP port for localhost listener (authorization code flow only)") - cmd.Flags().StringSliceVar(&flags.scopes, "scopes", []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID, "pinniped.sts.unrestricted"}, "OIDC scopes to request during login") + cmd.Flags().StringSliceVar(&flags.scopes, "scopes", []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID, "pinniped:request-audience"}, "OIDC scopes to request during login") cmd.Flags().BoolVar(&flags.skipBrowser, "skip-browser", false, "Skip opening the browser (just print the URL)") cmd.Flags().StringVar(&flags.sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file") cmd.Flags().StringSliceVar(&flags.caBundlePaths, "ca-bundle", nil, "Path to TLS certificate authority bundle (PEM format, optional, can be repeated)") cmd.Flags().StringSliceVar(&flags.caBundleData, "ca-bundle-data", nil, "Base64 endcoded TLS certificate authority bundle (base64 encoded PEM format, optional, can be repeated)") cmd.Flags().BoolVar(&flags.debugSessionCache, "debug-session-cache", false, "Print debug logs related to the session cache") - cmd.Flags().StringVar(&flags.requestAudience, "request-audience", "", "Request a token with an alternate audience using RF8693 token exchange") + cmd.Flags().StringVar(&flags.requestAudience, "request-audience", "", "Request a token with an alternate audience using RFC8693 token exchange") cmd.Flags().BoolVar(&flags.conciergeEnabled, "enable-concierge", false, "Exchange the OIDC ID token with the Pinniped concierge during login") cmd.Flags().StringVar(&flags.conciergeNamespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the concierge was installed") cmd.Flags().StringVar(&flags.conciergeAuthenticatorType, "concierge-authenticator-type", "", "Concierge authenticator type (e.g., 'webhook', 'jwt')") diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go index df4f8f9d..72626418 100644 --- a/cmd/pinniped/cmd/login_oidc_test.go +++ b/cmd/pinniped/cmd/login_oidc_test.go @@ -69,8 +69,8 @@ func TestLoginOIDCCommand(t *testing.T) { -h, --help help for oidc --issuer string OpenID Connect issuer URL --listen-port uint16 TCP port for localhost listener (authorization code flow only) - --request-audience string Request a token with an alternate audience using RF8693 token exchange - --scopes strings OIDC scopes to request during login (default [offline_access,openid,pinniped.sts.unrestricted]) + --request-audience string Request a token with an alternate audience using RFC8693 token exchange + --scopes strings OIDC scopes to request during login (default [offline_access,openid,pinniped:request-audience]) --session-cache string Path to session cache file (default "` + cfgDir + `/sessions.yaml") --skip-browser Skip opening the browser (just print the URL) `), diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 9375d6d6..f417f0c1 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -64,8 +64,8 @@ func NewHandler( // at this time, however we will temporarily grant the scope just in case that changes in a future release of fosite. oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOfflineAccess) - // Grant the Pinniped STS scope if requested. - oidc.GrantScopeIfRequested(authorizeRequester, "pinniped.sts.unrestricted") + // Grant the pinniped:request-audience scope if requested. + oidc.GrantScopeIfRequested(authorizeRequester, "pinniped:request-audience") now := time.Now() _, err = oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &openid.DefaultSession{ diff --git a/internal/oidc/callback/callback_handler.go b/internal/oidc/callback/callback_handler.go index c331e653..a8ad9b66 100644 --- a/internal/oidc/callback/callback_handler.go +++ b/internal/oidc/callback/callback_handler.go @@ -72,10 +72,10 @@ func NewHandler( return httperr.New(http.StatusBadRequest, "error using state downstream auth params") } - // Automatically grant the openid, offline_access, and Pinniped STS scopes, but only if they were requested. + // Automatically grant the openid, offline_access, and pinniped:request-audience scopes, but only if they were requested. oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOpenID) oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOfflineAccess) - oidc.GrantScopeIfRequested(authorizeRequester, "pinniped.sts.unrestricted") + oidc.GrantScopeIfRequested(authorizeRequester, "pinniped:request-audience") token, err := upstreamIDPConfig.ExchangeAuthcodeAndValidateTokens( r.Context(), diff --git a/internal/oidc/nullstorage_test.go b/internal/oidc/nullstorage_test.go index 523aafb5..74f1f8a1 100644 --- a/internal/oidc/nullstorage_test.go +++ b/internal/oidc/nullstorage_test.go @@ -28,7 +28,7 @@ func TestNullStorage_GetClient(t *testing.T) { RedirectURIs: []string{"http://127.0.0.1/callback"}, ResponseTypes: []string{"code"}, GrantTypes: []string{"authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange"}, - Scopes: []string{"openid", "offline_access", "profile", "email", "pinniped.sts.unrestricted"}, + Scopes: []string{"openid", "offline_access", "profile", "email", "pinniped:request-audience"}, }, TokenEndpointAuthMethod: "none", }, diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index a1cb56fe..152509aa 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -85,7 +85,7 @@ func PinnipedCLIOIDCClient() *fosite.DefaultOpenIDConnectClient { RedirectURIs: []string{"http://127.0.0.1/callback"}, ResponseTypes: []string{"code"}, GrantTypes: []string{"authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange"}, - Scopes: []string{coreosoidc.ScopeOpenID, coreosoidc.ScopeOfflineAccess, "profile", "email", "pinniped.sts.unrestricted"}, + Scopes: []string{coreosoidc.ScopeOpenID, coreosoidc.ScopeOfflineAccess, "profile", "email", "pinniped:request-audience"}, }, TokenEndpointAuthMethod: "none", } diff --git a/internal/oidc/token/token_handler_test.go b/internal/oidc/token/token_handler_test.go index f4610293..cddbb929 100644 --- a/internal/oidc/token/token_handler_test.go +++ b/internal/oidc/token/token_handler_test.go @@ -633,13 +633,13 @@ func TestTokenExchange(t *testing.T) { successfulAuthCodeExchange := tokenEndpointResponseExpectedValues{ wantStatus: http.StatusOK, wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"openid", "pinniped.sts.unrestricted"}, - wantGrantedScopes: []string{"openid", "pinniped.sts.unrestricted"}, + wantRequestedScopes: []string{"openid", "pinniped:request-audience"}, + wantGrantedScopes: []string{"openid", "pinniped:request-audience"}, } doValidAuthCodeExchange := authcodeExchangeInputs{ modifyAuthRequest: func(authRequest *http.Request) { - authRequest.Form.Set("scope", "openid pinniped.sts.unrestricted") + authRequest.Form.Set("scope", "openid pinniped:request-audience") }, want: successfulAuthCodeExchange, } @@ -730,7 +730,7 @@ func TestTokenExchange(t *testing.T) { wantResponseBodyContains: `invalid subject_token`, }, { - name: "access token missing pinniped.sts.unrestricted scope", + name: "access token missing pinniped:request-audience scope", authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(authRequest *http.Request) { authRequest.Form.Set("scope", "openid") @@ -744,19 +744,19 @@ func TestTokenExchange(t *testing.T) { }, requestedAudience: "some-workload-cluster", wantStatus: http.StatusForbidden, - wantResponseBodyContains: `missing the \"pinniped.sts.unrestricted\" scope`, + wantResponseBodyContains: `missing the \"pinniped:request-audience\" scope`, }, { name: "access token missing openid scope", authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(authRequest *http.Request) { - authRequest.Form.Set("scope", "pinniped.sts.unrestricted") + authRequest.Form.Set("scope", "pinniped:request-audience") }, want: tokenEndpointResponseExpectedValues{ wantStatus: http.StatusOK, wantSuccessBodyFields: []string{"access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"pinniped.sts.unrestricted"}, - wantGrantedScopes: []string{"pinniped.sts.unrestricted"}, + wantRequestedScopes: []string{"pinniped:request-audience"}, + wantGrantedScopes: []string{"pinniped:request-audience"}, }, }, requestedAudience: "some-workload-cluster", @@ -767,7 +767,7 @@ func TestTokenExchange(t *testing.T) { name: "token minting failure", authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(authRequest *http.Request) { - authRequest.Form.Set("scope", "openid pinniped.sts.unrestricted") + authRequest.Form.Set("scope", "openid pinniped:request-audience") }, // Fail to fetch a JWK signing key after the authcode exchange has happened. makeOathHelper: makeOauthHelperWithJWTKeyThatWorksOnlyOnce, @@ -918,23 +918,23 @@ func TestRefreshGrant(t *testing.T) { { name: "when the refresh request removes a scope which was originally granted from the list of requested scopes then it is granted anyway", authcodeExchange: authcodeExchangeInputs{ - modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access pinniped.sts.unrestricted") }, + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access pinniped:request-audience") }, want: tokenEndpointResponseExpectedValues{ wantStatus: http.StatusOK, wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"openid", "offline_access", "pinniped.sts.unrestricted"}, - wantGrantedScopes: []string{"openid", "offline_access", "pinniped.sts.unrestricted"}, + wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, + wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, }, }, refreshRequest: refreshRequestInputs{ modifyTokenRequest: func(r *http.Request, refreshToken string, accessToken string) { - r.Body = happyRefreshRequestBody(refreshToken).WithScope("openid").ReadCloser() // do not ask for "pinniped.sts.unrestricted" again + r.Body = happyRefreshRequestBody(refreshToken).WithScope("openid").ReadCloser() // do not ask for "pinniped:request-audience" again }, want: tokenEndpointResponseExpectedValues{ wantStatus: http.StatusOK, wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"openid", "offline_access", "pinniped.sts.unrestricted"}, - wantGrantedScopes: []string{"openid", "offline_access", "pinniped.sts.unrestricted"}, + wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, + wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, }}, }, { @@ -1400,8 +1400,8 @@ func simulateAuthEndpointHavingAlreadyRun(t *testing.T, authRequest *http.Reques if strings.Contains(authRequest.Form.Get("scope"), "offline_access") { authRequester.GrantScope("offline_access") } - if strings.Contains(authRequest.Form.Get("scope"), "pinniped.sts.unrestricted") { - authRequester.GrantScope("pinniped.sts.unrestricted") + if strings.Contains(authRequest.Form.Get("scope"), "pinniped:request-audience") { + authRequester.GrantScope("pinniped:request-audience") } authResponder, err := oauthHelper.NewAuthorizeResponse(ctx, authRequester, session) require.NoError(t, err) diff --git a/internal/oidc/token_exchange.go b/internal/oidc/token_exchange.go index 36ddfb83..8a933d37 100644 --- a/internal/oidc/token_exchange.go +++ b/internal/oidc/token_exchange.go @@ -18,7 +18,7 @@ import ( const ( tokenTypeAccessToken = "urn:ietf:params:oauth:token-type:access_token" //nolint: gosec tokenTypeJWT = "urn:ietf:params:oauth:token-type:jwt" //nolint: gosec - pinnipedTokenExchangeScope = "pinniped.sts.unrestricted" //nolint: gosec + pinnipedTokenExchangeScope = "pinniped:request-audience" //nolint: gosec ) type stsParams struct { @@ -65,7 +65,7 @@ func (t *TokenExchangeHandler) PopulateTokenEndpointResponse(ctx context.Context return errors.WithStack(err) } - // Require that the incoming access token has the STS and OpenID scopes. + // Require that the incoming access token has the pinniped:request-audience and OpenID scopes. if !originalRequester.GetGrantedScopes().Has(pinnipedTokenExchangeScope) { return errors.WithStack(fosite.ErrAccessDenied.WithHintf("missing the %q scope", pinnipedTokenExchangeScope)) } diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index 5ae1bf0c..9718a297 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -159,7 +159,7 @@ func WithClient(httpClient *http.Client) Option { } } -// WithRequestAudience causes the login flow to perform an additional token exchange using the RFC8693 STS flow. +// WithRequestAudience causes the login flow to perform an additional token exchange using the RFC8693 flow. func WithRequestAudience(audience string) Option { return func(h *handlerState) error { h.requestedAudience = audience diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 6fc03a7d..eb2e28cd 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -133,7 +133,7 @@ func TestSupervisorLogin(t *testing.T) { ClientID: "pinniped-cli", Endpoint: discovery.Endpoint(), RedirectURL: localCallbackServer.URL, - Scopes: []string{"openid", "pinniped.sts.unrestricted", "offline_access"}, + Scopes: []string{"openid", "pinniped:request-audience", "offline_access"}, } // Build a valid downstream authorize URL for the supervisor. @@ -175,7 +175,7 @@ func TestSupervisorLogin(t *testing.T) { callback := localCallbackServer.waitForCallback(10 * time.Second) t.Logf("got callback request: %s", library.MaskTokens(callback.URL.String())) require.Equal(t, stateParam.String(), callback.URL.Query().Get("state")) - require.ElementsMatch(t, []string{"openid", "pinniped.sts.unrestricted", "offline_access"}, strings.Split(callback.URL.Query().Get("scope"), " ")) + require.ElementsMatch(t, []string{"openid", "pinniped:request-audience", "offline_access"}, strings.Split(callback.URL.Query().Get("scope"), " ")) authcode := callback.URL.Query().Get("code") require.NotEmpty(t, authcode) From 5367fd9fcbecc7b23f059b5cc34d4665c2f9d9a2 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Wed, 16 Dec 2020 15:13:28 -0600 Subject: [PATCH 50/51] Trigger CI From 111f6513ac91e10472f117259a686ef950858ca8 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 15 Dec 2020 13:37:11 -0600 Subject: [PATCH 51/51] Upgrade base images to Debian 10.7-slim. Signed-off-by: Matt Moyer --- Dockerfile | 2 +- hack/lib/tilt/Tiltfile | 2 +- hack/lib/tilt/concierge.Dockerfile | 2 +- hack/lib/tilt/local-user-authenticator.Dockerfile | 2 +- hack/lib/tilt/supervisor.Dockerfile | 2 +- test/deploy/dex/proxy.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 73c50799..fdec5b50 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,7 @@ RUN mkdir out \ && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o out ./cmd/local-user-authenticator/... # Use a runtime image based on Debian slim -FROM debian:10.6-slim +FROM debian:10.7-slim RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* # Copy the binaries from the build-env stage diff --git a/hack/lib/tilt/Tiltfile b/hack/lib/tilt/Tiltfile index 6bd2e479..8233f665 100644 --- a/hack/lib/tilt/Tiltfile +++ b/hack/lib/tilt/Tiltfile @@ -148,7 +148,7 @@ k8s_yaml(local([ '--data-value namespace=concierge ' + '--data-value image_repo=image/concierge ' + '--data-value image_tag=tilt-dev ' + - '--data-value kube_cert_agent_image=debian:10.6-slim ' + + '--data-value kube_cert_agent_image=debian:10.7-slim ' + '--data-value discovery_url=$(TERM=dumb kubectl cluster-info | awk \'/master|control plane/ {print $NF}\') ' + '--data-value log_level=debug ' + '--data-value-yaml replicas=1 ' + diff --git a/hack/lib/tilt/concierge.Dockerfile b/hack/lib/tilt/concierge.Dockerfile index c2927bdf..e9daadd6 100644 --- a/hack/lib/tilt/concierge.Dockerfile +++ b/hack/lib/tilt/concierge.Dockerfile @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # Use a runtime image based on Debian slim -FROM debian:10.6-slim +FROM debian:10.7-slim # Copy the binary which was built outside the container. COPY build/pinniped-concierge /usr/local/bin/pinniped-concierge diff --git a/hack/lib/tilt/local-user-authenticator.Dockerfile b/hack/lib/tilt/local-user-authenticator.Dockerfile index b63a212a..6b8430f6 100644 --- a/hack/lib/tilt/local-user-authenticator.Dockerfile +++ b/hack/lib/tilt/local-user-authenticator.Dockerfile @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # Use a runtime image based on Debian slim -FROM debian:10.6-slim +FROM debian:10.7-slim # Copy the binary which was built outside the container. COPY build/local-user-authenticator /usr/local/bin/local-user-authenticator diff --git a/hack/lib/tilt/supervisor.Dockerfile b/hack/lib/tilt/supervisor.Dockerfile index 468749ad..916d009a 100644 --- a/hack/lib/tilt/supervisor.Dockerfile +++ b/hack/lib/tilt/supervisor.Dockerfile @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # Use a runtime image based on Debian slim -FROM debian:10.6-slim +FROM debian:10.7-slim RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* diff --git a/test/deploy/dex/proxy.yaml b/test/deploy/dex/proxy.yaml index be4d4878..84b4307c 100644 --- a/test/deploy/dex/proxy.yaml +++ b/test/deploy/dex/proxy.yaml @@ -48,7 +48,7 @@ spec: periodSeconds: 5 failureThreshold: 2 - name: accesslogs - image: debian:10.6-slim + image: debian:10.7-slim command: - "/bin/sh" - "-c"