Merge pull request #273 from vmware-tanzu/secret-generation
Generate secrets for Pinniped Supervisor
This commit is contained in:
commit
72ce69410e
@ -59,6 +59,30 @@ type OIDCProviderSpec struct {
|
|||||||
TLS *OIDCProviderTLSSpec `json:"tls,omitempty"`
|
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.
|
// OIDCProviderStatus is a struct that describes the actual state of an OIDC Provider.
|
||||||
type OIDCProviderStatus struct {
|
type OIDCProviderStatus struct {
|
||||||
// Status holds an enum that describes the state of this OIDC Provider. Note that this Status can
|
// 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
|
// +optional
|
||||||
LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"`
|
LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"`
|
||||||
|
|
||||||
// JWKSSecret holds the name of the secret in which this OIDC Provider's signing/verification keys
|
// Secrets contains information about this OIDC Provider's secrets.
|
||||||
// are stored. If it is empty, then the signing/verification keys are either unknown or they don't
|
|
||||||
// exist.
|
|
||||||
// +optional
|
// +optional
|
||||||
JWKSSecret corev1.LocalObjectReference `json:"jwksSecret,omitempty"`
|
Secrets OIDCProviderSecrets `json:"secrets,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OIDCProvider describes the configuration of an OIDC provider.
|
// OIDCProvider describes the configuration of an OIDC provider.
|
||||||
|
@ -5,6 +5,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@ -14,6 +15,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"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"
|
"k8s.io/apimachinery/pkg/util/clock"
|
||||||
kubeinformers "k8s.io/client-go/informers"
|
kubeinformers "k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
@ -28,6 +33,7 @@ import (
|
|||||||
pinnipedinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions"
|
pinnipedinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions"
|
||||||
"go.pinniped.dev/internal/config/supervisor"
|
"go.pinniped.dev/internal/config/supervisor"
|
||||||
"go.pinniped.dev/internal/controller/supervisorconfig"
|
"go.pinniped.dev/internal/controller/supervisorconfig"
|
||||||
|
"go.pinniped.dev/internal/controller/supervisorconfig/generator"
|
||||||
"go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatcher"
|
"go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatcher"
|
||||||
"go.pinniped.dev/internal/controller/supervisorstorage"
|
"go.pinniped.dev/internal/controller/supervisorstorage"
|
||||||
"go.pinniped.dev/internal/controllerlib"
|
"go.pinniped.dev/internal/controllerlib"
|
||||||
@ -70,6 +76,7 @@ func waitForSignal() os.Signal {
|
|||||||
return <-signalCh
|
return <-signalCh
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:funlen
|
||||||
func startControllers(
|
func startControllers(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
cfg *supervisor.Config,
|
cfg *supervisor.Config,
|
||||||
@ -77,11 +84,16 @@ func startControllers(
|
|||||||
dynamicJWKSProvider jwks.DynamicJWKSProvider,
|
dynamicJWKSProvider jwks.DynamicJWKSProvider,
|
||||||
dynamicTLSCertProvider provider.DynamicTLSCertProvider,
|
dynamicTLSCertProvider provider.DynamicTLSCertProvider,
|
||||||
dynamicUpstreamIDPProvider provider.DynamicUpstreamIDPProvider,
|
dynamicUpstreamIDPProvider provider.DynamicUpstreamIDPProvider,
|
||||||
|
secretCache *secret.Cache,
|
||||||
|
supervisorDeployment *appsv1.Deployment,
|
||||||
kubeClient kubernetes.Interface,
|
kubeClient kubernetes.Interface,
|
||||||
pinnipedClient pinnipedclientset.Interface,
|
pinnipedClient pinnipedclientset.Interface,
|
||||||
kubeInformers kubeinformers.SharedInformerFactory,
|
kubeInformers kubeinformers.SharedInformerFactory,
|
||||||
pinnipedInformers pinnipedinformers.SharedInformerFactory,
|
pinnipedInformers pinnipedinformers.SharedInformerFactory,
|
||||||
) {
|
) {
|
||||||
|
opInformer := pinnipedInformers.Config().V1alpha1().OIDCProviders()
|
||||||
|
secretInformer := kubeInformers.Core().V1().Secrets()
|
||||||
|
|
||||||
// Create controller manager.
|
// Create controller manager.
|
||||||
controllerManager := controllerlib.
|
controllerManager := controllerlib.
|
||||||
NewManager().
|
NewManager().
|
||||||
@ -99,7 +111,7 @@ func startControllers(
|
|||||||
issuerManager,
|
issuerManager,
|
||||||
clock.RealClock{},
|
clock.RealClock{},
|
||||||
pinnipedClient,
|
pinnipedClient,
|
||||||
pinnipedInformers.Config().V1alpha1().OIDCProviders(),
|
opInformer,
|
||||||
controllerlib.WithInformer,
|
controllerlib.WithInformer,
|
||||||
),
|
),
|
||||||
singletonWorker,
|
singletonWorker,
|
||||||
@ -109,8 +121,8 @@ func startControllers(
|
|||||||
cfg.Labels,
|
cfg.Labels,
|
||||||
kubeClient,
|
kubeClient,
|
||||||
pinnipedClient,
|
pinnipedClient,
|
||||||
kubeInformers.Core().V1().Secrets(),
|
secretInformer,
|
||||||
pinnipedInformers.Config().V1alpha1().OIDCProviders(),
|
opInformer,
|
||||||
controllerlib.WithInformer,
|
controllerlib.WithInformer,
|
||||||
),
|
),
|
||||||
singletonWorker,
|
singletonWorker,
|
||||||
@ -118,8 +130,8 @@ func startControllers(
|
|||||||
WithController(
|
WithController(
|
||||||
supervisorconfig.NewJWKSObserverController(
|
supervisorconfig.NewJWKSObserverController(
|
||||||
dynamicJWKSProvider,
|
dynamicJWKSProvider,
|
||||||
kubeInformers.Core().V1().Secrets(),
|
secretInformer,
|
||||||
pinnipedInformers.Config().V1alpha1().OIDCProviders(),
|
opInformer,
|
||||||
controllerlib.WithInformer,
|
controllerlib.WithInformer,
|
||||||
),
|
),
|
||||||
singletonWorker,
|
singletonWorker,
|
||||||
@ -128,8 +140,83 @@ func startControllers(
|
|||||||
supervisorconfig.NewTLSCertObserverController(
|
supervisorconfig.NewTLSCertObserverController(
|
||||||
dynamicTLSCertProvider,
|
dynamicTLSCertProvider,
|
||||||
cfg.NamesConfig.DefaultTLSCertificateSecret,
|
cfg.NamesConfig.DefaultTLSCertificateSecret,
|
||||||
kubeInformers.Core().V1().Secrets(),
|
secretInformer,
|
||||||
pinnipedInformers.Config().V1alpha1().OIDCProviders(),
|
opInformer,
|
||||||
|
controllerlib.WithInformer,
|
||||||
|
),
|
||||||
|
singletonWorker,
|
||||||
|
).
|
||||||
|
WithController(
|
||||||
|
generator.NewSupervisorSecretsController(
|
||||||
|
supervisorDeployment,
|
||||||
|
cfg.Labels,
|
||||||
|
kubeClient,
|
||||||
|
secretInformer,
|
||||||
|
func(secret []byte) {
|
||||||
|
plog.Debug("setting csrf cookie secret")
|
||||||
|
secretCache.SetCSRFCookieEncoderHashKey(secret)
|
||||||
|
},
|
||||||
|
controllerlib.WithInformer,
|
||||||
|
controllerlib.WithInitialEvent,
|
||||||
|
),
|
||||||
|
singletonWorker,
|
||||||
|
).
|
||||||
|
WithController(
|
||||||
|
generator.NewOIDCProviderSecretsController(
|
||||||
|
generator.NewSymmetricSecretHelper(
|
||||||
|
"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,
|
||||||
|
),
|
||||||
|
singletonWorker,
|
||||||
|
).
|
||||||
|
WithController(
|
||||||
|
generator.NewOIDCProviderSecretsController(
|
||||||
|
generator.NewSymmetricSecretHelper(
|
||||||
|
"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,
|
||||||
|
),
|
||||||
|
singletonWorker,
|
||||||
|
).
|
||||||
|
WithController(
|
||||||
|
generator.NewOIDCProviderSecretsController(
|
||||||
|
generator.NewSymmetricSecretHelper(
|
||||||
|
"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,
|
controllerlib.WithInformer,
|
||||||
),
|
),
|
||||||
singletonWorker,
|
singletonWorker,
|
||||||
@ -153,6 +240,41 @@ func startControllers(
|
|||||||
go controllerManager.Start(ctx)
|
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) {
|
func newClients() (kubernetes.Interface, pinnipedclientset.Interface, error) {
|
||||||
kubeConfig, err := restclient.InClusterConfig()
|
kubeConfig, err := restclient.InClusterConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -174,7 +296,9 @@ func newClients() (kubernetes.Interface, pinnipedclientset.Interface, error) {
|
|||||||
return kubeClient, pinnipedClient, nil
|
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())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -204,15 +328,22 @@ func run(serverInstallationNamespace string, cfg *supervisor.Config) error {
|
|||||||
dynamicJWKSProvider := jwks.NewDynamicJWKSProvider()
|
dynamicJWKSProvider := jwks.NewDynamicJWKSProvider()
|
||||||
dynamicTLSCertProvider := provider.NewDynamicTLSCertProvider()
|
dynamicTLSCertProvider := provider.NewDynamicTLSCertProvider()
|
||||||
dynamicUpstreamIDPProvider := provider.NewDynamicUpstreamIDPProvider()
|
dynamicUpstreamIDPProvider := provider.NewDynamicUpstreamIDPProvider()
|
||||||
|
secretCache := secret.Cache{}
|
||||||
|
|
||||||
// OIDC endpoints will be served by the oidProvidersManager, and any non-OIDC paths will fallback to the healthMux.
|
// OIDC endpoints will be served by the oidProvidersManager, and any non-OIDC paths will fallback to the healthMux.
|
||||||
oidProvidersManager := manager.NewManager(
|
oidProvidersManager := manager.NewManager(
|
||||||
healthMux,
|
healthMux,
|
||||||
dynamicJWKSProvider,
|
dynamicJWKSProvider,
|
||||||
dynamicUpstreamIDPProvider,
|
dynamicUpstreamIDPProvider,
|
||||||
|
&secretCache,
|
||||||
kubeClient.CoreV1().Secrets(serverInstallationNamespace),
|
kubeClient.CoreV1().Secrets(serverInstallationNamespace),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
supervisorDeployment, err := getSupervisorDeployment(ctx, kubeClient, podInfo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot get supervisor deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
startControllers(
|
startControllers(
|
||||||
ctx,
|
ctx,
|
||||||
cfg,
|
cfg,
|
||||||
@ -220,6 +351,8 @@ func run(serverInstallationNamespace string, cfg *supervisor.Config) error {
|
|||||||
dynamicJWKSProvider,
|
dynamicJWKSProvider,
|
||||||
dynamicTLSCertProvider,
|
dynamicTLSCertProvider,
|
||||||
dynamicUpstreamIDPProvider,
|
dynamicUpstreamIDPProvider,
|
||||||
|
&secretCache,
|
||||||
|
supervisorDeployment,
|
||||||
kubeClient,
|
kubeClient,
|
||||||
pinnipedClient,
|
pinnipedClient,
|
||||||
kubeInformers,
|
kubeInformers,
|
||||||
@ -288,7 +421,7 @@ func main() {
|
|||||||
klog.Fatal(fmt.Errorf("could not load config: %w", err))
|
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)
|
klog.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,17 +81,6 @@ spec:
|
|||||||
status:
|
status:
|
||||||
description: Status of the OIDC provider.
|
description: Status of the OIDC provider.
|
||||||
properties:
|
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:
|
lastUpdateTime:
|
||||||
description: LastUpdateTime holds the time at which the Status was
|
description: LastUpdateTime holds the time at which the Status was
|
||||||
last updated. It is a pointer to get around some undesirable behavior
|
last updated. It is a pointer to get around some undesirable behavior
|
||||||
@ -101,6 +90,51 @@ spec:
|
|||||||
message:
|
message:
|
||||||
description: Message provides human-readable details about the Status.
|
description: Message provides human-readable details about the Status.
|
||||||
type: string
|
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:
|
status:
|
||||||
description: Status holds an enum that describes the state of this
|
description: Status holds an enum that describes the state of this
|
||||||
OIDC Provider. Note that this Status can represent success or failure.
|
OIDC Provider. Note that this Status can represent success or failure.
|
||||||
|
@ -132,6 +132,9 @@ spec:
|
|||||||
- path: "namespace"
|
- path: "namespace"
|
||||||
fieldRef:
|
fieldRef:
|
||||||
fieldPath: metadata.namespace
|
fieldPath: metadata.namespace
|
||||||
|
- path: "name"
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
#! This will help make sure our multiple pods run on different nodes, making
|
#! This will help make sure our multiple pods run on different nodes, making
|
||||||
#! our deployment "more" "HA".
|
#! our deployment "more" "HA".
|
||||||
affinity:
|
affinity:
|
||||||
|
@ -25,6 +25,14 @@ rules:
|
|||||||
- apiGroups: [idp.supervisor.pinniped.dev]
|
- apiGroups: [idp.supervisor.pinniped.dev]
|
||||||
resources: [upstreamoidcproviders/status]
|
resources: [upstreamoidcproviders/status]
|
||||||
verbs: [get, patch, update]
|
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
|
kind: RoleBinding
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
22
generated/1.17/README.adoc
generated
22
generated/1.17/README.adoc
generated
@ -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"]
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcproviderspec"]
|
||||||
==== 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.
|
| *`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.
|
| *`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).
|
| *`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.
|
||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,6 +59,30 @@ type OIDCProviderSpec struct {
|
|||||||
TLS *OIDCProviderTLSSpec `json:"tls,omitempty"`
|
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.
|
// OIDCProviderStatus is a struct that describes the actual state of an OIDC Provider.
|
||||||
type OIDCProviderStatus struct {
|
type OIDCProviderStatus struct {
|
||||||
// Status holds an enum that describes the state of this OIDC Provider. Note that this Status can
|
// 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
|
// +optional
|
||||||
LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"`
|
LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"`
|
||||||
|
|
||||||
// JWKSSecret holds the name of the secret in which this OIDC Provider's signing/verification keys
|
// Secrets contains information about this OIDC Provider's secrets.
|
||||||
// are stored. If it is empty, then the signing/verification keys are either unknown or they don't
|
|
||||||
// exist.
|
|
||||||
// +optional
|
// +optional
|
||||||
JWKSSecret corev1.LocalObjectReference `json:"jwksSecret,omitempty"`
|
Secrets OIDCProviderSecrets `json:"secrets,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OIDCProvider describes the configuration of an OIDC provider.
|
// OIDCProvider describes the configuration of an OIDC provider.
|
||||||
|
@ -72,6 +72,26 @@ func (in *OIDCProviderList) DeepCopyObject() runtime.Object {
|
|||||||
return nil
|
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.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *OIDCProviderSpec) DeepCopyInto(out *OIDCProviderSpec) {
|
func (in *OIDCProviderSpec) DeepCopyInto(out *OIDCProviderSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
@ -100,7 +120,7 @@ func (in *OIDCProviderStatus) DeepCopyInto(out *OIDCProviderStatus) {
|
|||||||
in, out := &in.LastUpdateTime, &out.LastUpdateTime
|
in, out := &in.LastUpdateTime, &out.LastUpdateTime
|
||||||
*out = (*in).DeepCopy()
|
*out = (*in).DeepCopy()
|
||||||
}
|
}
|
||||||
out.JWKSSecret = in.JWKSSecret
|
out.Secrets = in.Secrets
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,17 +81,6 @@ spec:
|
|||||||
status:
|
status:
|
||||||
description: Status of the OIDC provider.
|
description: Status of the OIDC provider.
|
||||||
properties:
|
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:
|
lastUpdateTime:
|
||||||
description: LastUpdateTime holds the time at which the Status was
|
description: LastUpdateTime holds the time at which the Status was
|
||||||
last updated. It is a pointer to get around some undesirable behavior
|
last updated. It is a pointer to get around some undesirable behavior
|
||||||
@ -101,6 +90,51 @@ spec:
|
|||||||
message:
|
message:
|
||||||
description: Message provides human-readable details about the Status.
|
description: Message provides human-readable details about the Status.
|
||||||
type: string
|
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:
|
status:
|
||||||
description: Status holds an enum that describes the state of this
|
description: Status holds an enum that describes the state of this
|
||||||
OIDC Provider. Note that this Status can represent success or failure.
|
OIDC Provider. Note that this Status can represent success or failure.
|
||||||
|
22
generated/1.18/README.adoc
generated
22
generated/1.18/README.adoc
generated
@ -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"]
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-config-v1alpha1-oidcproviderspec"]
|
||||||
==== 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.
|
| *`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.
|
| *`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).
|
| *`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.
|
||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,6 +59,30 @@ type OIDCProviderSpec struct {
|
|||||||
TLS *OIDCProviderTLSSpec `json:"tls,omitempty"`
|
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.
|
// OIDCProviderStatus is a struct that describes the actual state of an OIDC Provider.
|
||||||
type OIDCProviderStatus struct {
|
type OIDCProviderStatus struct {
|
||||||
// Status holds an enum that describes the state of this OIDC Provider. Note that this Status can
|
// 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
|
// +optional
|
||||||
LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"`
|
LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"`
|
||||||
|
|
||||||
// JWKSSecret holds the name of the secret in which this OIDC Provider's signing/verification keys
|
// Secrets contains information about this OIDC Provider's secrets.
|
||||||
// are stored. If it is empty, then the signing/verification keys are either unknown or they don't
|
|
||||||
// exist.
|
|
||||||
// +optional
|
// +optional
|
||||||
JWKSSecret corev1.LocalObjectReference `json:"jwksSecret,omitempty"`
|
Secrets OIDCProviderSecrets `json:"secrets,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OIDCProvider describes the configuration of an OIDC provider.
|
// OIDCProvider describes the configuration of an OIDC provider.
|
||||||
|
@ -72,6 +72,26 @@ func (in *OIDCProviderList) DeepCopyObject() runtime.Object {
|
|||||||
return nil
|
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.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *OIDCProviderSpec) DeepCopyInto(out *OIDCProviderSpec) {
|
func (in *OIDCProviderSpec) DeepCopyInto(out *OIDCProviderSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
@ -100,7 +120,7 @@ func (in *OIDCProviderStatus) DeepCopyInto(out *OIDCProviderStatus) {
|
|||||||
in, out := &in.LastUpdateTime, &out.LastUpdateTime
|
in, out := &in.LastUpdateTime, &out.LastUpdateTime
|
||||||
*out = (*in).DeepCopy()
|
*out = (*in).DeepCopy()
|
||||||
}
|
}
|
||||||
out.JWKSSecret = in.JWKSSecret
|
out.Secrets = in.Secrets
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,17 +81,6 @@ spec:
|
|||||||
status:
|
status:
|
||||||
description: Status of the OIDC provider.
|
description: Status of the OIDC provider.
|
||||||
properties:
|
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:
|
lastUpdateTime:
|
||||||
description: LastUpdateTime holds the time at which the Status was
|
description: LastUpdateTime holds the time at which the Status was
|
||||||
last updated. It is a pointer to get around some undesirable behavior
|
last updated. It is a pointer to get around some undesirable behavior
|
||||||
@ -101,6 +90,51 @@ spec:
|
|||||||
message:
|
message:
|
||||||
description: Message provides human-readable details about the Status.
|
description: Message provides human-readable details about the Status.
|
||||||
type: string
|
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:
|
status:
|
||||||
description: Status holds an enum that describes the state of this
|
description: Status holds an enum that describes the state of this
|
||||||
OIDC Provider. Note that this Status can represent success or failure.
|
OIDC Provider. Note that this Status can represent success or failure.
|
||||||
|
22
generated/1.19/README.adoc
generated
22
generated/1.19/README.adoc
generated
@ -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"]
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-config-v1alpha1-oidcproviderspec"]
|
||||||
==== 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.
|
| *`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.
|
| *`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).
|
| *`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.
|
||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,6 +59,30 @@ type OIDCProviderSpec struct {
|
|||||||
TLS *OIDCProviderTLSSpec `json:"tls,omitempty"`
|
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.
|
// OIDCProviderStatus is a struct that describes the actual state of an OIDC Provider.
|
||||||
type OIDCProviderStatus struct {
|
type OIDCProviderStatus struct {
|
||||||
// Status holds an enum that describes the state of this OIDC Provider. Note that this Status can
|
// 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
|
// +optional
|
||||||
LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"`
|
LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"`
|
||||||
|
|
||||||
// JWKSSecret holds the name of the secret in which this OIDC Provider's signing/verification keys
|
// Secrets contains information about this OIDC Provider's secrets.
|
||||||
// are stored. If it is empty, then the signing/verification keys are either unknown or they don't
|
|
||||||
// exist.
|
|
||||||
// +optional
|
// +optional
|
||||||
JWKSSecret corev1.LocalObjectReference `json:"jwksSecret,omitempty"`
|
Secrets OIDCProviderSecrets `json:"secrets,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OIDCProvider describes the configuration of an OIDC provider.
|
// OIDCProvider describes the configuration of an OIDC provider.
|
||||||
|
@ -72,6 +72,26 @@ func (in *OIDCProviderList) DeepCopyObject() runtime.Object {
|
|||||||
return nil
|
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.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *OIDCProviderSpec) DeepCopyInto(out *OIDCProviderSpec) {
|
func (in *OIDCProviderSpec) DeepCopyInto(out *OIDCProviderSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
@ -100,7 +120,7 @@ func (in *OIDCProviderStatus) DeepCopyInto(out *OIDCProviderStatus) {
|
|||||||
in, out := &in.LastUpdateTime, &out.LastUpdateTime
|
in, out := &in.LastUpdateTime, &out.LastUpdateTime
|
||||||
*out = (*in).DeepCopy()
|
*out = (*in).DeepCopy()
|
||||||
}
|
}
|
||||||
out.JWKSSecret = in.JWKSSecret
|
out.Secrets = in.Secrets
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,17 +81,6 @@ spec:
|
|||||||
status:
|
status:
|
||||||
description: Status of the OIDC provider.
|
description: Status of the OIDC provider.
|
||||||
properties:
|
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:
|
lastUpdateTime:
|
||||||
description: LastUpdateTime holds the time at which the Status was
|
description: LastUpdateTime holds the time at which the Status was
|
||||||
last updated. It is a pointer to get around some undesirable behavior
|
last updated. It is a pointer to get around some undesirable behavior
|
||||||
@ -101,6 +90,51 @@ spec:
|
|||||||
message:
|
message:
|
||||||
description: Message provides human-readable details about the Status.
|
description: Message provides human-readable details about the Status.
|
||||||
type: string
|
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:
|
status:
|
||||||
description: Status holds an enum that describes the state of this
|
description: Status holds an enum that describes the state of this
|
||||||
OIDC Provider. Note that this Status can represent success or failure.
|
OIDC Provider. Note that this Status can represent success or failure.
|
||||||
|
104
internal/controller/supervisorconfig/generator/generator.go
Normal file
104
internal/controller/supervisorconfig/generator/generator.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
// 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 (
|
||||||
|
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, labels map[string]string) bool {
|
||||||
|
if secret.Type != symmetricSecretType {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
data, ok := secret.Data[symmetricSecretDataKey]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(data) != symmetricKeySize {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range labels {
|
||||||
|
if secret.Labels[key] != value {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func secretDataFunc() (map[string][]byte, error) {
|
||||||
|
symmetricKey, err := generateKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string][]byte{
|
||||||
|
symmetricSecretDataKey: 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",
|
||||||
|
}
|
||||||
|
|
||||||
|
blockOwnerDeletion := true
|
||||||
|
isController := false
|
||||||
|
|
||||||
|
return &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
OwnerReferences: []metav1.OwnerReference{
|
||||||
|
{
|
||||||
|
APIVersion: deploymentGVK.GroupVersion().String(),
|
||||||
|
Kind: deploymentGVK.Kind,
|
||||||
|
Name: owner.GetName(),
|
||||||
|
UID: owner.GetUID(),
|
||||||
|
BlockOwnerDeletion: &blockOwnerDeletion,
|
||||||
|
Controller: &isController,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Labels: labels,
|
||||||
|
},
|
||||||
|
Type: symmetricSecretType,
|
||||||
|
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
|
||||||
|
}
|
@ -0,0 +1,232 @@
|
|||||||
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type oidcProviderSecretsController struct {
|
||||||
|
secretHelper SecretHelper
|
||||||
|
kubeClient kubernetes.Interface
|
||||||
|
pinnipedClient pinnipedclientset.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(
|
||||||
|
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: fmt.Sprintf("%s%s", secretHelper.NamePrefix(), "controller"),
|
||||||
|
Syncer: &oidcProviderSecretsController{
|
||||||
|
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.
|
||||||
|
withInformer(
|
||||||
|
secretInformer,
|
||||||
|
pinnipedcontroller.SimpleFilter(isOPControllee, func(obj metav1.Object) controllerlib.Key {
|
||||||
|
if isOPControllee(obj) {
|
||||||
|
controller := metav1.GetControllerOf(obj)
|
||||||
|
return controllerlib.Key{
|
||||||
|
Name: controller.Name,
|
||||||
|
Namespace: obj.GetNamespace(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return controllerlib.Key{}
|
||||||
|
}),
|
||||||
|
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 {
|
||||||
|
op, 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 OP should have been garbage collected since it should have
|
||||||
|
// had this OP as its owner.
|
||||||
|
plog.Debug(
|
||||||
|
"oidcprovider deleted",
|
||||||
|
"oidcprovider",
|
||||||
|
klog.KRef(ctx.Key.Namespace, ctx.Key.Name),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
newSecret, err := c.secretHelper.Generate(op)
|
||||||
|
if err != nil {
|
||||||
|
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.KObj(op),
|
||||||
|
"secret",
|
||||||
|
klog.KObj(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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(op.Namespace).Get(secretName)
|
||||||
|
notFound := k8serrors.IsNotFound(err)
|
||||||
|
if err != nil && !notFound {
|
||||||
|
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, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.secretHelper.IsValid(op, secret) {
|
||||||
|
// If this secret is invalid, we need to generate a new one.
|
||||||
|
return true, secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *oidcProviderSecretsController) createOrUpdateSecret(
|
||||||
|
ctx context.Context,
|
||||||
|
op *configv1alpha1.OIDCProvider,
|
||||||
|
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("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{})
|
||||||
|
if err != nil {
|
||||||
|
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 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.Labels = (*newSecret).Labels
|
||||||
|
oldSecret.Type = (*newSecret).Type
|
||||||
|
oldSecret.Data = (*newSecret).Data
|
||||||
|
*newSecret = oldSecret
|
||||||
|
_, err = secretClient.Update(ctx, oldSecret, metav1.UpdateOptions{})
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,625 @@
|
|||||||
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package generator
|
||||||
|
|
||||||
|
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"
|
||||||
|
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/mocks/mocksecrethelper"
|
||||||
|
"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()
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
t.Cleanup(ctrl.Finish)
|
||||||
|
secretHelper := mocksecrethelper.NewMockSecretHelper(ctrl)
|
||||||
|
secretHelper.EXPECT().NamePrefix().Times(1).Return("some-name")
|
||||||
|
|
||||||
|
secretInformer := kubeinformers.NewSharedInformerFactory(
|
||||||
|
kubernetesfake.NewSimpleClientset(),
|
||||||
|
0,
|
||||||
|
).Core().V1().Secrets()
|
||||||
|
opcInformer := pinnipedinformers.NewSharedInformerFactory(
|
||||||
|
pinnipedfake.NewSimpleClientset(),
|
||||||
|
0,
|
||||||
|
).Config().V1alpha1().OIDCProviders()
|
||||||
|
withInformer := testutil.NewObservableWithInformerOption()
|
||||||
|
_ = NewOIDCProviderSecretsController(
|
||||||
|
secretHelper,
|
||||||
|
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()
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
t.Cleanup(ctrl.Finish)
|
||||||
|
secretHelper := mocksecrethelper.NewMockSecretHelper(ctrl)
|
||||||
|
secretHelper.EXPECT().NamePrefix().Times(1).Return("some-name")
|
||||||
|
|
||||||
|
secretInformer := kubeinformers.NewSharedInformerFactory(
|
||||||
|
kubernetesfake.NewSimpleClientset(),
|
||||||
|
0,
|
||||||
|
).Core().V1().Secrets()
|
||||||
|
opcInformer := pinnipedinformers.NewSharedInformerFactory(
|
||||||
|
pinnipedfake.NewSimpleClientset(),
|
||||||
|
0,
|
||||||
|
).Config().V1alpha1().OIDCProviders()
|
||||||
|
withInformer := testutil.NewObservableWithInformerOption()
|
||||||
|
_ = NewOIDCProviderSecretsController(
|
||||||
|
secretHelper,
|
||||||
|
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 TestOIDCProviderSecretsControllerSync(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
namespace = "some-namespace"
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
secretGVR := schema.GroupVersionResource{
|
||||||
|
Group: corev1.SchemeGroupVersion.Group,
|
||||||
|
Version: corev1.SchemeGroupVersion.Version,
|
||||||
|
Resource: "secrets",
|
||||||
|
}
|
||||||
|
|
||||||
|
goodOP := &configv1alpha1.OIDCProvider{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: opName,
|
||||||
|
Namespace: namespace,
|
||||||
|
UID: opUID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
goodOPWithStatus := goodOP.DeepCopy()
|
||||||
|
goodOPWithStatus.Status.Secrets.TokenSigningKey.Name = goodSecret.Name
|
||||||
|
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
storage func(**configv1alpha1.OIDCProvider, **corev1.Secret)
|
||||||
|
client func(*pinnipedfake.Clientset, *kubernetesfake.Clientset)
|
||||||
|
secretHelper func(*mocksecrethelper.MockSecretHelper)
|
||||||
|
wantOPActions []kubetesting.Action
|
||||||
|
wantSecretActions []kubetesting.Action
|
||||||
|
wantError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OIDCProvider does not exist and secret does not exist",
|
||||||
|
storage: func(op **configv1alpha1.OIDCProvider, s **corev1.Secret) {
|
||||||
|
*op = nil
|
||||||
|
*s = nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OIDCProvider does not exist 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().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 invalid secret exists",
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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"))
|
||||||
|
},
|
||||||
|
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().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 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(_ *pinnipedfake.Clientset, c *kubernetesfake.Clientset) {
|
||||||
|
c.PrependReactor("get", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
||||||
|
return true, nil, errors.New("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: "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(_ *pinnipedfake.Clientset, c *kubernetesfake.Clientset) {
|
||||||
|
c.PrependReactor("create", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
||||||
|
return true, nil, errors.New("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: "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(_ *pinnipedfake.Clientset, c *kubernetesfake.Clientset) {
|
||||||
|
c.PrependReactor("update", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
||||||
|
return true, nil, errors.New("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: "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().ObserveActiveSecretAndUpdateParentOIDCProvider(goodOP, goodSecret).Times(1).Return(goodOPWithStatus)
|
||||||
|
},
|
||||||
|
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),
|
||||||
|
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
||||||
|
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
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
pinnipedAPIClient := pinnipedfake.NewSimpleClientset()
|
||||||
|
pinnipedInformerClient := pinnipedfake.NewSimpleClientset()
|
||||||
|
|
||||||
|
kubeAPIClient := kubernetesfake.NewSimpleClientset()
|
||||||
|
kubeInformerClient := kubernetesfake.NewSimpleClientset()
|
||||||
|
|
||||||
|
op := goodOP.DeepCopy()
|
||||||
|
secret := goodSecret.DeepCopy()
|
||||||
|
if test.storage != nil {
|
||||||
|
test.storage(&op, &secret)
|
||||||
|
}
|
||||||
|
if op != nil {
|
||||||
|
require.NoError(t, pinnipedAPIClient.Tracker().Add(op))
|
||||||
|
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.client != nil {
|
||||||
|
test.client(pinnipedAPIClient, kubeAPIClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
kubeInformers := kubeinformers.NewSharedInformerFactory(
|
||||||
|
kubeInformerClient,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(
|
||||||
|
pinnipedInformerClient,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
t.Cleanup(ctrl.Finish)
|
||||||
|
secretHelper := mocksecrethelper.NewMockSecretHelper(ctrl)
|
||||||
|
secretHelper.EXPECT().NamePrefix().Times(1).Return("some-name")
|
||||||
|
if test.secretHelper != nil {
|
||||||
|
test.secretHelper(secretHelper)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := NewOIDCProviderSecretsController(
|
||||||
|
secretHelper,
|
||||||
|
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: controllerlib.Key{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: opName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if test.wantError != "" {
|
||||||
|
require.EqualError(t, err, test.wantError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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{}
|
||||||
|
}
|
||||||
|
require.Equal(t, test.wantSecretActions, kubeAPIClient.Actions())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolPtr(b bool) *bool { return &b }
|
151
internal/controller/supervisorconfig/generator/secret_helper.go
Normal file
151
internal/controller/supervisorconfig/generator/secret_helper.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package generator
|
||||||
|
|
||||||
|
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/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 NamePrefix() that can be used to identify it from other SecretHelper instances.
|
||||||
|
type SecretHelper interface {
|
||||||
|
NamePrefix() string
|
||||||
|
Generate(*configv1alpha1.OIDCProvider) (*corev1.Secret, error)
|
||||||
|
IsValid(*configv1alpha1.OIDCProvider, *corev1.Secret) bool
|
||||||
|
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"
|
||||||
|
|
||||||
|
// 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).
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type symmetricSecretHelper struct {
|
||||||
|
namePrefix string
|
||||||
|
labels map[string]string
|
||||||
|
rand io.Reader
|
||||||
|
secretUsage SecretUsage
|
||||||
|
updateCacheFunc func(cacheKey string, cacheValue []byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *symmetricSecretHelper) NamePrefix() string { return s.namePrefix }
|
||||||
|
|
||||||
|
// Generate implements SecretHelper.Generate().
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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: symmetricSecretType,
|
||||||
|
Data: map[string][]byte{
|
||||||
|
symmetricSecretDataKey: key,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid implements SecretHelper.IsValid().
|
||||||
|
func (s *symmetricSecretHelper) IsValid(parent *configv1alpha1.OIDCProvider, secret *corev1.Secret) bool {
|
||||||
|
if !metav1.IsControlledBy(secret, parent) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if secret.Type != symmetricSecretType {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
key, ok := secret.Data[symmetricSecretDataKey]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(key) != symmetricKeySize {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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])
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
@ -0,0 +1,197 @@
|
|||||||
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package generator
|
||||||
|
|
||||||
|
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 TestSymmetricSecretHelper(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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()
|
||||||
|
|
||||||
|
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
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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 TestSymmetricSecretHelperIsValid(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 := NewSymmetricSecretHelper("none of these args matter", nil, nil, SecretUsageTokenSigningKey, 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,145 @@
|
|||||||
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Package secretgenerator provides a supervisorSecretsController that can ensure existence of a generated secret.
|
||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"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"
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// generateKey is stubbed out for the purpose of testing. The default behavior is to generate a symmetric key.
|
||||||
|
//nolint:gochecknoglobals
|
||||||
|
var generateKey = generateSymmetricKey
|
||||||
|
|
||||||
|
type supervisorSecretsController struct {
|
||||||
|
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(
|
||||||
|
owner *appsv1.Deployment,
|
||||||
|
labels map[string]string,
|
||||||
|
kubeClient kubernetes.Interface,
|
||||||
|
secretInformer corev1informers.SecretInformer,
|
||||||
|
setCacheFunc func(secret []byte),
|
||||||
|
withInformer pinnipedcontroller.WithInformerOptionFunc,
|
||||||
|
initialEventFunc pinnipedcontroller.WithInitialEventOptionFunc,
|
||||||
|
) controllerlib.Controller {
|
||||||
|
c := supervisorSecretsController{
|
||||||
|
owner: owner,
|
||||||
|
labels: labels,
|
||||||
|
kubeClient: kubeClient,
|
||||||
|
secretInformer: secretInformer,
|
||||||
|
setCacheFunc: setCacheFunc,
|
||||||
|
}
|
||||||
|
return controllerlib.New(
|
||||||
|
controllerlib.Config{Name: owner.Name + "-secret-generator", Syncer: &c},
|
||||||
|
withInformer(
|
||||||
|
secretInformer,
|
||||||
|
pinnipedcontroller.SimpleFilter(func(obj metav1.Object) bool {
|
||||||
|
ownerReferences := obj.GetOwnerReferences()
|
||||||
|
for i := range obj.GetOwnerReferences() {
|
||||||
|
if ownerReferences[i].UID == owner.GetUID() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, nil),
|
||||||
|
controllerlib.InformerOption{},
|
||||||
|
),
|
||||||
|
initialEventFunc(controllerlib.Key{
|
||||||
|
Namespace: owner.Namespace,
|
||||||
|
Name: owner.Name + "-key",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync implements controllerlib.Syncer.Sync().
|
||||||
|
func (c *supervisorSecretsController) Sync(ctx controllerlib.Context) error {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretNeedsUpdate := isNotFound || !isValid(secret, c.labels)
|
||||||
|
if !secretNeedsUpdate {
|
||||||
|
plog.Debug("secret is up to date", "secret", klog.KObj(secret))
|
||||||
|
c.setCacheFunc(secret.Data[symmetricSecretDataKey])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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", newSecret.Namespace, newSecret.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.setCacheFunc(newSecret.Data[symmetricSecretDataKey])
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *supervisorSecretsController) createSecret(ctx context.Context, newSecret *corev1.Secret) error {
|
||||||
|
_, 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.kubeClient.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 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
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,578 @@
|
|||||||
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"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"
|
||||||
|
"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"
|
||||||
|
"go.pinniped.dev/internal/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
owner = &appsv1.Deployment{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "some-owner-name",
|
||||||
|
Namespace: "some-namespace",
|
||||||
|
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: "owner reference is missing",
|
||||||
|
secret: corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "some-namespace",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "owner reference with incorrect `APIVersion`",
|
||||||
|
secret: corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "some-namespace",
|
||||||
|
OwnerReferences: []metav1.OwnerReference{
|
||||||
|
{
|
||||||
|
Name: owner.GetName(),
|
||||||
|
Kind: ownerGVK.Kind,
|
||||||
|
UID: owner.GetUID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAdd: true,
|
||||||
|
wantUpdate: true,
|
||||||
|
wantDelete: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "owner reference with incorrect `Kind`",
|
||||||
|
secret: corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "some-namespace",
|
||||||
|
OwnerReferences: []metav1.OwnerReference{
|
||||||
|
{
|
||||||
|
APIVersion: ownerGVK.String(),
|
||||||
|
Name: owner.GetName(),
|
||||||
|
Kind: "IncorrectKind",
|
||||||
|
UID: owner.GetUID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAdd: true,
|
||||||
|
wantUpdate: true,
|
||||||
|
wantDelete: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "owner reference with `Controller`: true",
|
||||||
|
secret: corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "some-namespace",
|
||||||
|
OwnerReferences: []metav1.OwnerReference{
|
||||||
|
*metav1.NewControllerRef(owner, ownerGVK),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAdd: true,
|
||||||
|
wantUpdate: true,
|
||||||
|
wantDelete: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
OwnerReferences: []metav1.OwnerReference{
|
||||||
|
{
|
||||||
|
Kind: "UnrelatedKind",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIVersion: ownerGVK.String(),
|
||||||
|
Name: owner.GetName(),
|
||||||
|
Kind: ownerGVK.Kind,
|
||||||
|
UID: owner.GetUID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
testutil.NewObservableWithInitialEventOption().WithInitialEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
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 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{
|
||||||
|
Namespace: owner.Namespace,
|
||||||
|
Name: owner.Name + "-key",
|
||||||
|
}, initialEventOption.GetInitialEventKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSupervisorSecretsControllerSync(t *testing.T) {
|
||||||
|
const (
|
||||||
|
generatedSecretNamespace = "some-namespace"
|
||||||
|
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")
|
||||||
|
otherGeneratedSymmetricKey = []byte("some-funio-32-byte-generated-key")
|
||||||
|
|
||||||
|
blockOwnerDeletion = true
|
||||||
|
isController = false
|
||||||
|
generatedSecret = &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: generatedSecretName,
|
||||||
|
Namespace: generatedSecretNamespace,
|
||||||
|
OwnerReferences: []metav1.OwnerReference{
|
||||||
|
{
|
||||||
|
APIVersion: ownerGVK.GroupVersion().String(),
|
||||||
|
Kind: ownerGVK.Kind,
|
||||||
|
Name: owner.GetName(),
|
||||||
|
UID: owner.GetUID(),
|
||||||
|
BlockOwnerDeletion: &blockOwnerDeletion,
|
||||||
|
Controller: &isController,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Labels: labels,
|
||||||
|
},
|
||||||
|
Type: "secrets.pinniped.dev/symmetric",
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"key": generatedSymmetricKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
otherGeneratedSecret = &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: generatedSecretName,
|
||||||
|
Namespace: generatedSecretNamespace,
|
||||||
|
OwnerReferences: []metav1.OwnerReference{
|
||||||
|
{
|
||||||
|
APIVersion: ownerGVK.GroupVersion().String(),
|
||||||
|
Kind: ownerGVK.Kind,
|
||||||
|
Name: owner.GetName(),
|
||||||
|
UID: owner.GetUID(),
|
||||||
|
BlockOwnerDeletion: &blockOwnerDeletion,
|
||||||
|
Controller: &isController,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Labels: labels,
|
||||||
|
},
|
||||||
|
Type: "secrets.pinniped.dev/symmetric",
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"key": otherGeneratedSymmetricKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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",
|
||||||
|
storedSecret: func(secret **corev1.Secret) {
|
||||||
|
*secret = nil
|
||||||
|
},
|
||||||
|
wantActions: []kubetesting.Action{
|
||||||
|
kubetesting.NewCreateAction(secretsGVR, generatedSecretNamespace, generatedSecret),
|
||||||
|
},
|
||||||
|
wantCallbackSecret: generatedSymmetricKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when a valid secret exists, nothing happens",
|
||||||
|
wantCallbackSecret: generatedSymmetricKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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, generatedSecret),
|
||||||
|
},
|
||||||
|
wantCallbackSecret: generatedSymmetricKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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, generatedSecret),
|
||||||
|
},
|
||||||
|
wantCallbackSecret: generatedSymmetricKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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, generatedSecret),
|
||||||
|
},
|
||||||
|
wantCallbackSecret: generatedSymmetricKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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, generatedSecret),
|
||||||
|
},
|
||||||
|
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, generatedSecret),
|
||||||
|
kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName),
|
||||||
|
kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecret),
|
||||||
|
},
|
||||||
|
wantCallbackSecret: generatedSymmetricKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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, otherGeneratedSecret, nil
|
||||||
|
})
|
||||||
|
},
|
||||||
|
wantActions: []kubetesting.Action{
|
||||||
|
kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName),
|
||||||
|
},
|
||||||
|
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) {
|
||||||
|
(*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),
|
||||||
|
},
|
||||||
|
wantCallbackSecret: generatedSymmetricKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
storedSecret: func(secret **corev1.Secret) {
|
||||||
|
*secret = nil
|
||||||
|
},
|
||||||
|
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 := generatedSecret.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()
|
||||||
|
|
||||||
|
var callbackSecret []byte
|
||||||
|
c := NewSupervisorSecretsController(
|
||||||
|
owner,
|
||||||
|
labels,
|
||||||
|
apiClient,
|
||||||
|
secrets,
|
||||||
|
func(secret []byte) {
|
||||||
|
require.Nil(t, callbackSecret, "callback was called twice")
|
||||||
|
callbackSecret = secret
|
||||||
|
},
|
||||||
|
testutil.NewObservableWithInformerOption().WithInformer,
|
||||||
|
testutil.NewObservableWithInitialEventOption().WithInitialEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
|
||||||
|
require.Equal(t, test.wantCallbackSecret, callbackSecret)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -76,7 +76,7 @@ func (c *jwksObserverController) Sync(ctx controllerlib.Context) error {
|
|||||||
issuerToActiveJWKMap := map[string]*jose.JSONWebKey{}
|
issuerToActiveJWKMap := map[string]*jose.JSONWebKey{}
|
||||||
|
|
||||||
for _, provider := range allProviders {
|
for _, provider := range allProviders {
|
||||||
secretRef := provider.Status.JWKSSecret
|
secretRef := provider.Status.Secrets.JWKS
|
||||||
jwksSecret, err := c.secretInformer.Lister().Secrets(ns).Get(secretRef.Name)
|
jwksSecret, err := c.secretInformer.Lister().Secrets(ns).Get(secretRef.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
plog.Debug("jwksObserverController Sync could not find JWKS secret", "namespace", ns, "secretName", secretRef.Name)
|
plog.Debug("jwksObserverController Sync could not find JWKS secret", "namespace", ns, "secretName", secretRef.Name)
|
||||||
|
@ -202,7 +202,7 @@ func TestJWKSObserverControllerSync(t *testing.T) {
|
|||||||
Namespace: installedInNamespace,
|
Namespace: installedInNamespace,
|
||||||
},
|
},
|
||||||
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://no-secret-issuer1.com"},
|
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{
|
oidcProviderWithoutSecret2 := &v1alpha1.OIDCProvider{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
@ -219,7 +219,9 @@ func TestJWKSObserverControllerSync(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://bad-secret-issuer.com"},
|
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://bad-secret-issuer.com"},
|
||||||
Status: v1alpha1.OIDCProviderStatus{
|
Status: v1alpha1.OIDCProviderStatus{
|
||||||
JWKSSecret: corev1.LocalObjectReference{Name: "bad-secret-name"},
|
Secrets: v1alpha1.OIDCProviderSecrets{
|
||||||
|
JWKS: corev1.LocalObjectReference{Name: "bad-secret-name"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
oidcProviderWithBadJWKSSecret := &v1alpha1.OIDCProvider{
|
oidcProviderWithBadJWKSSecret := &v1alpha1.OIDCProvider{
|
||||||
@ -229,7 +231,9 @@ func TestJWKSObserverControllerSync(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://bad-jwks-secret-issuer.com"},
|
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://bad-jwks-secret-issuer.com"},
|
||||||
Status: v1alpha1.OIDCProviderStatus{
|
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{
|
oidcProviderWithBadActiveJWKSecret := &v1alpha1.OIDCProvider{
|
||||||
@ -239,7 +243,9 @@ func TestJWKSObserverControllerSync(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://bad-active-jwk-secret-issuer.com"},
|
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://bad-active-jwk-secret-issuer.com"},
|
||||||
Status: v1alpha1.OIDCProviderStatus{
|
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{
|
oidcProviderWithGoodSecret1 := &v1alpha1.OIDCProvider{
|
||||||
@ -249,7 +255,9 @@ func TestJWKSObserverControllerSync(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://issuer-with-good-secret1.com"},
|
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://issuer-with-good-secret1.com"},
|
||||||
Status: v1alpha1.OIDCProviderStatus{
|
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{
|
oidcProviderWithGoodSecret2 := &v1alpha1.OIDCProvider{
|
||||||
@ -259,7 +267,9 @@ func TestJWKSObserverControllerSync(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://issuer-with-good-secret2.com"},
|
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://issuer-with-good-secret2.com"},
|
||||||
Status: v1alpha1.OIDCProviderStatus{
|
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"))
|
expectedJWK1 = string(readJWKJSON(t, "testdata/public-jwk.json"))
|
||||||
|
@ -169,7 +169,7 @@ func (c *jwksWriterController) Sync(ctx controllerlib.Context) error {
|
|||||||
|
|
||||||
// Ensure that the OPC points to the secret.
|
// Ensure that the OPC points to the secret.
|
||||||
newOPC := opc.DeepCopy()
|
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 {
|
if err := c.updateOPC(ctx.Context, newOPC); err != nil {
|
||||||
return fmt.Errorf("cannot update opc: %w", err)
|
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) {
|
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.
|
// If the OPC says it doesn't have a secret associated with it, then let's create one.
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// This OPC says it has a secret associated with it. Let's try to get it from the cache.
|
// 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)
|
notFound := k8serrors.IsNotFound(err)
|
||||||
if err != nil && !notFound {
|
if err != nil && !notFound {
|
||||||
return false, fmt.Errorf("cannot get secret: %w", err)
|
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)
|
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.
|
// If the existing OPC is up to date, we don't need to update it.
|
||||||
return nil
|
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{})
|
_, err = opcClient.Update(ctx, oldOPC, metav1.UpdateOptions{})
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
@ -253,7 +253,7 @@ func TestJWKSWriterControllerSync(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
goodOPCWithStatus := goodOPC.DeepCopy()
|
goodOPCWithStatus := goodOPC.DeepCopy()
|
||||||
goodOPCWithStatus.Status.JWKSSecret.Name = goodOPCWithStatus.Name + "-jwks"
|
goodOPCWithStatus.Status.Secrets.JWKS.Name = goodOPCWithStatus.Name + "-jwks"
|
||||||
|
|
||||||
secretGVR := schema.GroupVersionResource{
|
secretGVR := schema.GroupVersionResource{
|
||||||
Group: corev1.SchemeGroupVersion.Group,
|
Group: corev1.SchemeGroupVersion.Group,
|
||||||
@ -264,7 +264,7 @@ func TestJWKSWriterControllerSync(t *testing.T) {
|
|||||||
newSecret := func(activeJWKPath, jwksPath string) *corev1.Secret {
|
newSecret := func(activeJWKPath, jwksPath string) *corev1.Secret {
|
||||||
s := corev1.Secret{
|
s := corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: goodOPCWithStatus.Status.JWKSSecret.Name,
|
Name: goodOPCWithStatus.Status.Secrets.JWKS.Name,
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
"myLabelKey1": "myLabelValue1",
|
"myLabelKey1": "myLabelValue1",
|
||||||
|
@ -13,6 +13,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/plog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PodInfo contains pod metadata about the current pod.
|
// PodInfo contains pod metadata about the current pod.
|
||||||
@ -20,6 +22,9 @@ type PodInfo struct {
|
|||||||
// Namespace where the current pod is running.
|
// Namespace where the current pod is running.
|
||||||
Namespace string
|
Namespace string
|
||||||
|
|
||||||
|
// Name of the current pod.
|
||||||
|
Name string
|
||||||
|
|
||||||
// Labels of the current pod.
|
// Labels of the current pod.
|
||||||
Labels map[string]string
|
Labels map[string]string
|
||||||
}
|
}
|
||||||
@ -33,6 +38,13 @@ func Load(directory string) (*PodInfo, error) {
|
|||||||
}
|
}
|
||||||
result.Namespace = strings.TrimSpace(string(ns))
|
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"))
|
labels, err := ioutil.ReadFile(filepath.Join(directory, "labels"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not load labels: %w", err)
|
return nil, fmt.Errorf("could not load labels: %w", err)
|
||||||
|
@ -35,6 +35,15 @@ func TestLoad(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "valid",
|
name: "valid",
|
||||||
inputDir: "./testdata/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{
|
want: &PodInfo{
|
||||||
Namespace: "test-namespace",
|
Namespace: "test-namespace",
|
||||||
Labels: map[string]string{"foo": "bar", "bat": "baz"},
|
Labels: map[string]string{"foo": "bar", "bat": "baz"},
|
||||||
|
1
internal/downward/testdata/valid/name
vendored
Normal file
1
internal/downward/testdata/valid/name
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
test-name
|
2
internal/downward/testdata/validwithoutname/labels
vendored
Normal file
2
internal/downward/testdata/validwithoutname/labels
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
foo="bar"
|
||||||
|
bat="baz"
|
1
internal/downward/testdata/validwithoutname/namespace
vendored
Normal file
1
internal/downward/testdata/validwithoutname/namespace
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
test-namespace
|
6
internal/mocks/mocksecrethelper/generate.go
Normal file
6
internal/mocks/mocksecrethelper/generate.go
Normal file
@ -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
|
96
internal/mocks/mocksecrethelper/mocksecrethelper.go
Normal file
96
internal/mocks/mocksecrethelper/mocksecrethelper.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamePrefix mocks base method
|
||||||
|
func (m *MockSecretHelper) NamePrefix() string {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "NamePrefix")
|
||||||
|
ret0, _ := ret[0].(string)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, "NamePrefix", reflect.TypeOf((*MockSecretHelper)(nil).NamePrefix))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObserveActiveSecretAndUpdateParentOIDCProvider mocks base method
|
||||||
|
func (m *MockSecretHelper) ObserveActiveSecretAndUpdateParentOIDCProvider(arg0 *v1alpha1.OIDCProvider, arg1 *v1.Secret) *v1alpha1.OIDCProvider {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "ObserveActiveSecretAndUpdateParentOIDCProvider", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(*v1alpha1.OIDCProvider)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, "ObserveActiveSecretAndUpdateParentOIDCProvider", reflect.TypeOf((*MockSecretHelper)(nil).ObserveActiveSecretAndUpdateParentOIDCProvider), arg0, arg1)
|
||||||
|
}
|
@ -127,10 +127,10 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
|
|
||||||
// Configure fosite the same way that the production code would, using NullStorage to turn off storage.
|
// Configure fosite the same way that the production code would, using NullStorage to turn off storage.
|
||||||
oauthStore := oidc.NullStorage{}
|
oauthStore := oidc.NullStorage{}
|
||||||
hmacSecret := []byte("some secret - must have at least 32 bytes")
|
hmacSecretFunc := func() []byte { return []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")
|
require.GreaterOrEqual(t, len(hmacSecretFunc()), 32, "fosite requires that hmac secrets have at least 32 bytes")
|
||||||
jwksProviderIsUnused := jwks.NewDynamicJWKSProvider()
|
jwksProviderIsUnused := jwks.NewDynamicJWKSProvider()
|
||||||
oauthHelper := oidc.FositeOauth2Helper(oauthStore, downstreamIssuer, hmacSecret, jwksProviderIsUnused, oidc.DefaultOIDCTimeoutsConfiguration())
|
oauthHelper := oidc.FositeOauth2Helper(oauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, oidc.DefaultOIDCTimeoutsConfiguration())
|
||||||
|
|
||||||
happyCSRF := "test-csrf"
|
happyCSRF := "test-csrf"
|
||||||
happyPKCE := "test-pkce"
|
happyPKCE := "test-pkce"
|
||||||
|
@ -461,10 +461,10 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
// Inject this into our test subject at the last second so we get a fresh storage for every test.
|
// Inject this into our test subject at the last second so we get a fresh storage for every test.
|
||||||
timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration()
|
timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration()
|
||||||
oauthStore := oidc.NewKubeStorage(secrets, timeoutsConfiguration)
|
oauthStore := oidc.NewKubeStorage(secrets, timeoutsConfiguration)
|
||||||
hmacSecret := []byte("some secret - must have at least 32 bytes")
|
hmacSecretFunc := func() []byte { return []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")
|
require.GreaterOrEqual(t, len(hmacSecretFunc()), 32, "fosite requires that hmac secrets have at least 32 bytes")
|
||||||
jwksProviderIsUnused := jwks.NewDynamicJWKSProvider()
|
jwksProviderIsUnused := jwks.NewDynamicJWKSProvider()
|
||||||
oauthHelper := oidc.FositeOauth2Helper(oauthStore, downstreamIssuer, hmacSecret, jwksProviderIsUnused, timeoutsConfiguration)
|
oauthHelper := oidc.FositeOauth2Helper(oauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, timeoutsConfiguration)
|
||||||
|
|
||||||
idpListGetter := oidctestutil.NewIDPListGetter(&test.idp)
|
idpListGetter := oidctestutil.NewIDPListGetter(&test.idp)
|
||||||
subject := NewHandler(idpListGetter, oauthHelper, happyStateCodec, happyCookieCodec, happyUpstreamRedirectURI)
|
subject := NewHandler(idpListGetter, oauthHelper, happyStateCodec, happyCookieCodec, happyUpstreamRedirectURI)
|
||||||
|
98
internal/oidc/dynamic_oauth2_hmac_strategy.go
Normal file
98
internal/oidc/dynamic_oauth2_hmac_strategy.go
Normal file
@ -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)
|
||||||
|
}
|
58
internal/oidc/dynamiccodec/codec.go
Normal file
58
internal/oidc/dynamiccodec/codec.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// 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 (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ oidc.Codec = &Codec{}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
lifespan time.Duration
|
||||||
|
signingKeyFunc KeyFunc
|
||||||
|
encryptionKeyFunc KeyFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode implements oidc.Encode().
|
||||||
|
func (c *Codec) Encode(name string, value interface{}) (string, error) {
|
||||||
|
return c.delegate().Encode(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode implements oidc.Decode().
|
||||||
|
func (c *Codec) Decode(name string, value string, into interface{}) error {
|
||||||
|
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
|
||||||
|
}
|
127
internal/oidc/dynamiccodec/codec_test.go
Normal file
127
internal/oidc/dynamiccodec/codec_test.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package dynamiccodec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCodec(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
lifespan time.Duration
|
||||||
|
keys func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte)
|
||||||
|
wantEncoderErrorPrefix 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")
|
||||||
|
},
|
||||||
|
wantEncoderErrorPrefix: "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: "aaa encoder times stuff out",
|
||||||
|
lifespan: time.Second,
|
||||||
|
wantDecoderError: "securecookie: expired timestamp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad encoder signing key",
|
||||||
|
keys: func(encoderSigningKey, encoderEncryptionKey, decoderSigningKey, decoderEncryptionKey *[]byte) {
|
||||||
|
*encoderSigningKey = nil
|
||||||
|
},
|
||||||
|
wantEncoderErrorPrefix: "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: ",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
if test.wantEncoderErrorPrefix != "" {
|
||||||
|
require.EqualError(t, err, test.wantEncoderErrorPrefix)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
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), "expected %q to start with %q", err.Error(), test.wantDecoderError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, "some-message", decoded)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -44,6 +44,11 @@ const (
|
|||||||
// CSRFCookieEncodingName is the `name` passed to the encoder for encoding and decoding the CSRF
|
// CSRFCookieEncodingName is the `name` passed to the encoder for encoding and decoding the CSRF
|
||||||
// cookie contents.
|
// cookie contents.
|
||||||
CSRFCookieEncodingName = "csrf"
|
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.
|
// Encoder is the encoding side of the securecookie.Codec interface.
|
||||||
@ -187,7 +192,7 @@ func DefaultOIDCTimeoutsConfiguration() TimeoutsConfiguration {
|
|||||||
func FositeOauth2Helper(
|
func FositeOauth2Helper(
|
||||||
oauthStore interface{},
|
oauthStore interface{},
|
||||||
issuer string,
|
issuer string,
|
||||||
hmacSecretOfLengthAtLeast32 []byte,
|
hmacSecretOfLengthAtLeast32Func func() []byte,
|
||||||
jwksProvider jwks.DynamicJWKSProvider,
|
jwksProvider jwks.DynamicJWKSProvider,
|
||||||
timeoutsConfiguration TimeoutsConfiguration,
|
timeoutsConfiguration TimeoutsConfiguration,
|
||||||
) fosite.OAuth2Provider {
|
) fosite.OAuth2Provider {
|
||||||
@ -220,7 +225,7 @@ func FositeOauth2Helper(
|
|||||||
oauthStore,
|
oauthStore,
|
||||||
&compose.CommonStrategy{
|
&compose.CommonStrategy{
|
||||||
// Note that Fosite requires the HMAC secret to be at least 32 bytes.
|
// 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),
|
OpenIDConnectTokenStrategy: newDynamicOpenIDConnectECDSAStrategy(oauthConfig, jwksProvider),
|
||||||
},
|
},
|
||||||
nil, // hasher, defaults to using BCrypt when nil. Used for hashing client secrets.
|
nil, // hasher, defaults to using BCrypt when nil. Used for hashing client secrets.
|
||||||
|
@ -8,7 +8,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/gorilla/securecookie"
|
"go.pinniped.dev/internal/secret"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/oidc/dynamiccodec"
|
||||||
|
|
||||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/oidc"
|
"go.pinniped.dev/internal/oidc"
|
||||||
@ -34,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
|
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
|
dynamicJWKSProvider jwks.DynamicJWKSProvider // in-memory cache of per-issuer JWKS data
|
||||||
idpListGetter oidc.IDPListGetter // in-memory cache of upstream IDPs
|
idpListGetter oidc.IDPListGetter // in-memory cache of upstream IDPs
|
||||||
|
secretCache *secret.Cache // in-memory cache of cryptographic material
|
||||||
secretsClient corev1client.SecretInterface
|
secretsClient corev1client.SecretInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +49,7 @@ func NewManager(
|
|||||||
nextHandler http.Handler,
|
nextHandler http.Handler,
|
||||||
dynamicJWKSProvider jwks.DynamicJWKSProvider,
|
dynamicJWKSProvider jwks.DynamicJWKSProvider,
|
||||||
idpListGetter oidc.IDPListGetter,
|
idpListGetter oidc.IDPListGetter,
|
||||||
|
secretCache *secret.Cache,
|
||||||
secretsClient corev1client.SecretInterface,
|
secretsClient corev1client.SecretInterface,
|
||||||
) *Manager {
|
) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
@ -52,6 +57,7 @@ func NewManager(
|
|||||||
nextHandler: nextHandler,
|
nextHandler: nextHandler,
|
||||||
dynamicJWKSProvider: dynamicJWKSProvider,
|
dynamicJWKSProvider: dynamicJWKSProvider,
|
||||||
idpListGetter: idpListGetter,
|
idpListGetter: idpListGetter,
|
||||||
|
secretCache: secretCache,
|
||||||
secretsClient: secretsClient,
|
secretsClient: secretsClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,33 +77,32 @@ func (m *Manager) SetProviders(oidcProviders ...*provider.OIDCProvider) {
|
|||||||
m.providers = oidcProviders
|
m.providers = oidcProviders
|
||||||
m.providerHandlers = make(map[string]http.Handler)
|
m.providerHandlers = make(map[string]http.Handler)
|
||||||
|
|
||||||
|
var csrfCookieEncoder = dynamiccodec.New(
|
||||||
|
oidc.CSRFCookieLifespan,
|
||||||
|
m.secretCache.GetCSRFCookieEncoderHashKey,
|
||||||
|
func() []byte { return nil },
|
||||||
|
)
|
||||||
|
|
||||||
for _, incomingProvider := range oidcProviders {
|
for _, incomingProvider := range oidcProviders {
|
||||||
issuer := incomingProvider.Issuer()
|
issuer := incomingProvider.Issuer()
|
||||||
issuerHostWithPath := strings.ToLower(incomingProvider.IssuerHost()) + "/" + incomingProvider.IssuerPath()
|
issuerHostWithPath := strings.ToLower(incomingProvider.IssuerHost()) + "/" + incomingProvider.IssuerPath()
|
||||||
|
|
||||||
fositeHMACSecretForThisProvider := []byte("some secret - must have at least 32 bytes") // TODO replace this secret
|
tokenHMACKeyGetter := wrapGetter(incomingProvider.Issuer(), m.secretCache.GetTokenHMACKey)
|
||||||
|
|
||||||
timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration()
|
timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration()
|
||||||
|
|
||||||
// Use NullStorage for the authorize endpoint because we do not actually want to store anything until
|
// Use NullStorage for the authorize endpoint because we do not actually want to store anything until
|
||||||
// the upstream callback endpoint is called later.
|
// the upstream callback endpoint is called later.
|
||||||
oauthHelperWithNullStorage := oidc.FositeOauth2Helper(oidc.NullStorage{}, issuer, fositeHMACSecretForThisProvider, nil, timeoutsConfiguration)
|
oauthHelperWithNullStorage := oidc.FositeOauth2Helper(oidc.NullStorage{}, issuer, tokenHMACKeyGetter, nil, timeoutsConfiguration)
|
||||||
|
|
||||||
// For all the other endpoints, make another oauth helper with exactly the same settings except use real storage.
|
// 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, timeoutsConfiguration), issuer, fositeHMACSecretForThisProvider, m.dynamicJWKSProvider, timeoutsConfiguration)
|
oauthHelperWithKubeStorage := oidc.FositeOauth2Helper(oidc.NewKubeStorage(m.secretsClient, timeoutsConfiguration), issuer, tokenHMACKeyGetter, m.dynamicJWKSProvider, timeoutsConfiguration)
|
||||||
|
|
||||||
// TODO use different codecs for the state and the cookie, because:
|
var upstreamStateEncoder = dynamiccodec.New(
|
||||||
// 1. we would like to state to have an embedded expiration date while the cookie does not need that
|
timeoutsConfiguration.UpstreamStateParamLifespan,
|
||||||
// 2. we would like each downstream provider to use different secrets for signing/encrypting the upstream state, not share secrets
|
wrapGetter(incomingProvider.Issuer(), m.secretCache.GetStateEncoderHashKey),
|
||||||
// 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)
|
wrapGetter(incomingProvider.Issuer(), m.secretCache.GetStateEncoderBlockKey),
|
||||||
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 csrfCookieEncoderHashKey = []byte("fake-csrf-hash-secret") // TODO replace this secret
|
|
||||||
var csrfCookieEncoder = securecookie.New(csrfCookieEncoderHashKey, nil)
|
|
||||||
csrfCookieEncoder.SetSerializer(securecookie.JSONEncoder{})
|
|
||||||
|
|
||||||
m.providerHandlers[(issuerHostWithPath + oidc.WellKnownEndpointPath)] = discovery.NewHandler(issuer)
|
m.providerHandlers[(issuerHostWithPath + oidc.WellKnownEndpointPath)] = discovery.NewHandler(issuer)
|
||||||
|
|
||||||
@ -154,3 +159,9 @@ func (m *Manager) findHandler(req *http.Request) http.Handler {
|
|||||||
|
|
||||||
return m.providerHandlers[strings.ToLower(req.Host)+"/"+req.URL.Path]
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -14,6 +14,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/secret"
|
||||||
|
|
||||||
"github.com/sclevine/spec"
|
"github.com/sclevine/spec"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
@ -241,7 +243,18 @@ func TestManager(t *testing.T) {
|
|||||||
kubeClient = fake.NewSimpleClientset()
|
kubeClient = fake.NewSimpleClientset()
|
||||||
secretsClient := kubeClient.CoreV1().Secrets("some-namespace")
|
secretsClient := kubeClient.CoreV1().Secrets("some-namespace")
|
||||||
|
|
||||||
subject = NewManager(nextHandler, dynamicJWKSProvider, idpListGetter, secretsClient)
|
cache := secret.Cache{}
|
||||||
|
cache.SetCSRFCookieEncoderHashKey([]byte("fake-csrf-hash-secret"))
|
||||||
|
|
||||||
|
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"))
|
||||||
|
|
||||||
|
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)
|
||||||
})
|
})
|
||||||
|
|
||||||
when("given no providers via SetProviders()", func() {
|
when("given no providers via SetProviders()", func() {
|
||||||
@ -304,21 +317,26 @@ func TestManager(t *testing.T) {
|
|||||||
requireAuthorizationRequestToBeHandled(issuer2, authRequestParams, upstreamIDPAuthorizationURL)
|
requireAuthorizationRequestToBeHandled(issuer2, authRequestParams, upstreamIDPAuthorizationURL)
|
||||||
|
|
||||||
// Hostnames are case-insensitive, so test that we can handle that.
|
// Hostnames are case-insensitive, so test that we can handle that.
|
||||||
|
csrfCookieValue1, upstreamStateParam1 :=
|
||||||
requireAuthorizationRequestToBeHandled(issuer1DifferentCaseHostname, authRequestParams, upstreamIDPAuthorizationURL)
|
requireAuthorizationRequestToBeHandled(issuer1DifferentCaseHostname, authRequestParams, upstreamIDPAuthorizationURL)
|
||||||
csrfCookieValue, upstreamStateParam :=
|
csrfCookieValue2, upstreamStateParam2 :=
|
||||||
requireAuthorizationRequestToBeHandled(issuer2DifferentCaseHostname, authRequestParams, upstreamIDPAuthorizationURL)
|
requireAuthorizationRequestToBeHandled(issuer2DifferentCaseHostname, authRequestParams, upstreamIDPAuthorizationURL)
|
||||||
|
|
||||||
callbackRequestParams := "?" + url.Values{
|
callbackRequestParams1 := "?" + url.Values{
|
||||||
"code": []string{"some-fake-code"},
|
"code": []string{"some-fake-code"},
|
||||||
"state": []string{upstreamStateParam},
|
"state": []string{upstreamStateParam1},
|
||||||
|
}.Encode()
|
||||||
|
callbackRequestParams2 := "?" + url.Values{
|
||||||
|
"code": []string{"some-fake-code"},
|
||||||
|
"state": []string{upstreamStateParam2},
|
||||||
}.Encode()
|
}.Encode()
|
||||||
|
|
||||||
downstreamAuthCode1 := requireCallbackRequestToBeHandled(issuer1, callbackRequestParams, csrfCookieValue)
|
downstreamAuthCode1 := requireCallbackRequestToBeHandled(issuer1, callbackRequestParams1, csrfCookieValue1)
|
||||||
downstreamAuthCode2 := requireCallbackRequestToBeHandled(issuer2, callbackRequestParams, csrfCookieValue)
|
downstreamAuthCode2 := requireCallbackRequestToBeHandled(issuer2, callbackRequestParams2, csrfCookieValue2)
|
||||||
|
|
||||||
// Hostnames are case-insensitive, so test that we can handle that.
|
// Hostnames are case-insensitive, so test that we can handle that.
|
||||||
downstreamAuthCode3 := requireCallbackRequestToBeHandled(issuer1DifferentCaseHostname, callbackRequestParams, csrfCookieValue)
|
downstreamAuthCode3 := requireCallbackRequestToBeHandled(issuer1DifferentCaseHostname, callbackRequestParams1, csrfCookieValue1)
|
||||||
downstreamAuthCode4 := requireCallbackRequestToBeHandled(issuer2DifferentCaseHostname, callbackRequestParams, csrfCookieValue)
|
downstreamAuthCode4 := requireCallbackRequestToBeHandled(issuer2DifferentCaseHostname, callbackRequestParams2, csrfCookieValue2)
|
||||||
|
|
||||||
requireTokenRequestToBeHandled(issuer1, downstreamAuthCode1, issuer1JWKS, issuer1)
|
requireTokenRequestToBeHandled(issuer1, downstreamAuthCode1, issuer1JWKS, issuer1)
|
||||||
requireTokenRequestToBeHandled(issuer2, downstreamAuthCode2, issuer2JWKS, issuer2)
|
requireTokenRequestToBeHandled(issuer2, downstreamAuthCode2, issuer2JWKS, issuer2)
|
||||||
@ -336,7 +354,7 @@ func TestManager(t *testing.T) {
|
|||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
subject.SetProviders(p1, p2)
|
subject.SetProviders(p1, p2)
|
||||||
|
|
||||||
jwks := map[string]*jose.JSONWebKeySet{
|
jwksMap := map[string]*jose.JSONWebKeySet{
|
||||||
issuer1: {Keys: []jose.JSONWebKey{*newTestJWK(issuer1KeyID)}},
|
issuer1: {Keys: []jose.JSONWebKey{*newTestJWK(issuer1KeyID)}},
|
||||||
issuer2: {Keys: []jose.JSONWebKey{*newTestJWK(issuer2KeyID)}},
|
issuer2: {Keys: []jose.JSONWebKey{*newTestJWK(issuer2KeyID)}},
|
||||||
}
|
}
|
||||||
@ -344,7 +362,7 @@ func TestManager(t *testing.T) {
|
|||||||
issuer1: newTestJWK(issuer1KeyID),
|
issuer1: newTestJWK(issuer1KeyID),
|
||||||
issuer2: newTestJWK(issuer2KeyID),
|
issuer2: newTestJWK(issuer2KeyID),
|
||||||
}
|
}
|
||||||
dynamicJWKSProvider.SetIssuerToJWKSMap(jwks, activeJWK)
|
dynamicJWKSProvider.SetIssuerToJWKSMap(jwksMap, activeJWK)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("sends all non-matching host requests to the nextHandler", func() {
|
it("sends all non-matching host requests to the nextHandler", func() {
|
||||||
@ -379,7 +397,7 @@ func TestManager(t *testing.T) {
|
|||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
subject.SetProviders(p2, p1)
|
subject.SetProviders(p2, p1)
|
||||||
|
|
||||||
jwks := map[string]*jose.JSONWebKeySet{
|
jwksMap := map[string]*jose.JSONWebKeySet{
|
||||||
issuer1: {Keys: []jose.JSONWebKey{*newTestJWK(issuer1KeyID)}},
|
issuer1: {Keys: []jose.JSONWebKey{*newTestJWK(issuer1KeyID)}},
|
||||||
issuer2: {Keys: []jose.JSONWebKey{*newTestJWK(issuer2KeyID)}},
|
issuer2: {Keys: []jose.JSONWebKey{*newTestJWK(issuer2KeyID)}},
|
||||||
}
|
}
|
||||||
@ -387,7 +405,7 @@ func TestManager(t *testing.T) {
|
|||||||
issuer1: newTestJWK(issuer1KeyID),
|
issuer1: newTestJWK(issuer1KeyID),
|
||||||
issuer2: newTestJWK(issuer2KeyID),
|
issuer2: newTestJWK(issuer2KeyID),
|
||||||
}
|
}
|
||||||
dynamicJWKSProvider.SetIssuerToJWKSMap(jwks, activeJWK)
|
dynamicJWKSProvider.SetIssuerToJWKSMap(jwksMap, activeJWK)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("still routes matching requests to the appropriate provider", func() {
|
it("still routes matching requests to the appropriate provider", func() {
|
||||||
|
@ -69,6 +69,10 @@ var (
|
|||||||
goodAuthTime = time.Date(1, 2, 3, 4, 5, 6, 7, time.UTC)
|
goodAuthTime = time.Date(1, 2, 3, 4, 5, 6, 7, time.UTC)
|
||||||
goodRequestedAtTime = time.Date(7, 6, 5, 4, 3, 2, 1, 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 {
|
fositeInvalidMethodErrorBody = func(actual string) string {
|
||||||
return here.Docf(`
|
return here.Docf(`
|
||||||
{
|
{
|
||||||
@ -1323,7 +1327,7 @@ func makeHappyOauthHelper(
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer)
|
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)
|
authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper)
|
||||||
return oauthHelper, authResponder.GetCode(), jwtSigningKey
|
return oauthHelper, authResponder.GetCode(), jwtSigningKey
|
||||||
}
|
}
|
||||||
@ -1355,7 +1359,7 @@ func makeOauthHelperWithJWTKeyThatWorksOnlyOnce(
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer)
|
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)
|
authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper)
|
||||||
return oauthHelper, authResponder.GetCode(), jwtSigningKey
|
return oauthHelper, authResponder.GetCode(), jwtSigningKey
|
||||||
}
|
}
|
||||||
@ -1374,7 +1378,7 @@ func makeOauthHelperWithNilPrivateJWTSigningKey(
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
jwkProvider := jwks.NewDynamicJWKSProvider() // empty provider which contains no signing key for this issuer
|
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)
|
authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper)
|
||||||
return oauthHelper, authResponder.GetCode(), nil
|
return oauthHelper, authResponder.GetCode(), nil
|
||||||
}
|
}
|
||||||
|
71
internal/secret/cache.go
Normal file
71
internal/secret/cache.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package secret
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cache struct {
|
||||||
|
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 bytesOrNil(c.csrfCookieEncoderHashKey.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) SetCSRFCookieEncoderHashKey(key []byte) {
|
||||||
|
c.csrfCookieEncoderHashKey.Store(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) GetTokenHMACKey(oidcIssuer string) []byte {
|
||||||
|
return bytesOrNil(c.getOIDCProviderCache(oidcIssuer).tokenHMACKey.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) SetTokenHMACKey(oidcIssuer string, key []byte) {
|
||||||
|
c.getOIDCProviderCache(oidcIssuer).tokenHMACKey.Store(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
value = &oidcProviderCache{}
|
||||||
|
c.oidcProviderCacheMap.Store(oidcIssuer, value)
|
||||||
|
}
|
||||||
|
return value.(*oidcProviderCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesOrNil(b interface{}) []byte {
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return b.([]byte)
|
||||||
|
}
|
106
internal/secret/cache_test.go
Normal file
106
internal/secret/cache_test.go
Normal file
@ -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())
|
||||||
|
}
|
@ -35,6 +35,11 @@ import (
|
|||||||
// TestE2EFullIntegration tests a full integration scenario that combines the supervisor, concierge, and CLI.
|
// TestE2EFullIntegration tests a full integration scenario that combines the supervisor, concierge, and CLI.
|
||||||
func TestE2EFullIntegration(t *testing.T) {
|
func TestE2EFullIntegration(t *testing.T) {
|
||||||
env := library.IntegrationEnv(t).WithCapability(library.ClusterSigningKeyIsAvailable)
|
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)
|
ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
defer cancelFunc()
|
defer cancelFunc()
|
||||||
|
|
||||||
@ -160,7 +165,15 @@ func TestE2EFullIntegration(t *testing.T) {
|
|||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
err := kubectlCmd.Wait()
|
err := kubectlCmd.Wait()
|
||||||
t.Logf("kubectl subprocess exited with code %d", kubectlCmd.ProcessState.ExitCode())
|
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("<error reading stdout: " + stdoutErr.Error() + ">")
|
||||||
|
}
|
||||||
|
stderr, stderrErr := ioutil.ReadAll(stderrPipe)
|
||||||
|
if stderrErr != nil {
|
||||||
|
stderr = []byte("<error reading stderr: " + stderrErr.Error() + ">")
|
||||||
|
}
|
||||||
|
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.
|
// 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")
|
require.Fail(t, "timed out waiting for kubectl output")
|
||||||
case kubectlOutput = <-kubectlOutputChan:
|
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())
|
t.Logf("first kubectl command took %s", time.Since(start).String())
|
||||||
|
|
||||||
// Run kubectl again, which should work with no browser interaction.
|
// Run kubectl again, which should work with no browser interaction.
|
||||||
|
@ -592,13 +592,16 @@ 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) {
|
func requireStatus(t *testing.T, client pinnipedclientset.Interface, ns, name string, status v1alpha1.OIDCProviderStatusCondition) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var opc *v1alpha1.OIDCProvider
|
var opc *v1alpha1.OIDCProvider
|
||||||
var err error
|
var err error
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
opc, err = client.ConfigV1alpha1().OIDCProviders(ns).Get(ctx, name, metav1.GetOptions{})
|
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
|
return err == nil && opc.Status.Status == status
|
||||||
}, 10*time.Second, 200*time.Millisecond)
|
}, 10*time.Second, 200*time.Millisecond)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -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).
|
|
||||||
}
|
|
@ -179,12 +179,8 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
authcode := callback.URL.Query().Get("code")
|
authcode := callback.URL.Query().Get("code")
|
||||||
require.NotEmpty(t, authcode)
|
require.NotEmpty(t, authcode)
|
||||||
|
|
||||||
// Call the token endpoint to get tokens. Give the Supervisor a couple of seconds to wire up its signing key.
|
// Call the token endpoint to get tokens.
|
||||||
var tokenResponse *oauth2.Token
|
tokenResponse, err := downstreamOAuth2Config.Exchange(oidcHTTPClientContext, authcode, pkceParam.Verifier())
|
||||||
assert.Eventually(t, func() bool {
|
|
||||||
tokenResponse, err = downstreamOAuth2Config.Exchange(oidcHTTPClientContext, authcode, pkceParam.Verifier())
|
|
||||||
return err == nil
|
|
||||||
}, time.Second*5, time.Second*1)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat"}
|
expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat"}
|
||||||
|
166
test/integration/supervisor_secrets_test.go
Normal file
166
test/integration/supervisor_secrets_test.go
Normal file
@ -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.Secrets.JWKS.Name
|
||||||
|
},
|
||||||
|
ensureValid: ensureValidJWKS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hmac signing secret",
|
||||||
|
secretName: func(op *configv1alpha1.OIDCProvider) string {
|
||||||
|
return op.Status.Secrets.TokenSigningKey.Name
|
||||||
|
},
|
||||||
|
ensureValid: ensureValidSymmetricKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "state signature secret",
|
||||||
|
secretName: func(op *configv1alpha1.OIDCProvider) string {
|
||||||
|
return op.Status.Secrets.StateSigningKey.Name
|
||||||
|
},
|
||||||
|
ensureValid: ensureValidSymmetricKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "state encryption secret",
|
||||||
|
secretName: func(op *configv1alpha1.OIDCProvider) string {
|
||||||
|
return op.Status.Secrets.StateEncryptionKey.Name
|
||||||
|
},
|
||||||
|
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))
|
||||||
|
}
|
@ -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)
|
}, 60*time.Second, 1*time.Second, "expected the OIDCProvider to have status %q", expectStatus)
|
||||||
require.Equal(t, expectStatus, result.Status.Status)
|
require.Equal(t, expectStatus, result.Status.Status)
|
||||||
|
|
||||||
|
// 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{})
|
||||||
|
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
|
return opc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user