84c3c3aa9c
- Add `AllowPasswordGrant` boolean field to OIDCIdentityProvider's spec - The oidc upstream watcher controller copies the value of `AllowPasswordGrant` into the configuration of the cached provider - Add password grant to the UpstreamOIDCIdentityProviderI interface which is implemented by the cached provider instance for use in the authorization endpoint - Enhance the IDP discovery endpoint to return the supported "flows" for each IDP ("cli_password" and/or "browser_authcode") - Enhance `pinniped get kubeconfig` to help the user choose the desired flow for the selected IDP, and to write the flow into the resulting kubeconfg - Enhance `pinniped login oidc` to have a flow flag to tell it which client-side flow it should use for auth (CLI-based or browser-based) - In the Dex config, allow the resource owner password grant, which Dex implements to also return ID tokens, for use in integration tests - Enhance the authorize endpoint to perform password grant when requested by the incoming headers. This commit does not include unit tests for the enhancements to the authorize endpoint, which will come in the next commit - Extract some shared helpers from the callback endpoint to share the code with the authorize endpoint - Add new integration tests
922 lines
46 KiB
Go
922 lines
46 KiB
Go
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package oidcupstreamwatcher
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/url"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/client-go/informers"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
|
|
"go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
|
pinnipedfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
|
|
pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions"
|
|
"go.pinniped.dev/internal/controllerlib"
|
|
"go.pinniped.dev/internal/oidc/provider"
|
|
"go.pinniped.dev/internal/testutil"
|
|
"go.pinniped.dev/internal/testutil/oidctestutil"
|
|
"go.pinniped.dev/internal/testutil/testlogger"
|
|
"go.pinniped.dev/internal/upstreamoidc"
|
|
)
|
|
|
|
func TestOIDCUpstreamWatcherControllerFilterSecret(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
secret metav1.Object
|
|
wantAdd bool
|
|
wantUpdate bool
|
|
wantDelete bool
|
|
}{
|
|
{
|
|
name: "a secret of the right type",
|
|
secret: &corev1.Secret{
|
|
Type: "secrets.pinniped.dev/oidc-client",
|
|
ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"},
|
|
},
|
|
wantAdd: true,
|
|
wantUpdate: true,
|
|
wantDelete: true,
|
|
},
|
|
{
|
|
name: "a secret of the wrong type",
|
|
secret: &corev1.Secret{
|
|
Type: "secrets.pinniped.dev/not-the-oidc-client-type",
|
|
ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"},
|
|
},
|
|
},
|
|
{
|
|
name: "resource of wrong data type",
|
|
secret: &corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"},
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fakePinnipedClient := pinnipedfake.NewSimpleClientset()
|
|
pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0)
|
|
fakeKubeClient := fake.NewSimpleClientset()
|
|
kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0)
|
|
testLog := testlogger.New(t)
|
|
cache := provider.NewDynamicUpstreamIDPProvider()
|
|
cache.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{
|
|
&upstreamoidc.ProviderConfig{Name: "initial-entry"},
|
|
})
|
|
secretInformer := kubeInformers.Core().V1().Secrets()
|
|
withInformer := testutil.NewObservableWithInformerOption()
|
|
|
|
New(
|
|
cache,
|
|
nil,
|
|
pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(),
|
|
secretInformer,
|
|
testLog,
|
|
withInformer.WithInformer,
|
|
)
|
|
|
|
unrelated := corev1.Secret{}
|
|
filter := withInformer.GetFilterForInformer(secretInformer)
|
|
require.Equal(t, test.wantAdd, filter.Add(test.secret))
|
|
require.Equal(t, test.wantUpdate, filter.Update(&unrelated, test.secret))
|
|
require.Equal(t, test.wantUpdate, filter.Update(test.secret, &unrelated))
|
|
require.Equal(t, test.wantDelete, filter.Delete(test.secret))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
|
|
t.Parallel()
|
|
now := metav1.NewTime(time.Now().UTC())
|
|
earlier := metav1.NewTime(now.Add(-1 * time.Hour).UTC())
|
|
|
|
// Start another test server that answers discovery successfully.
|
|
testIssuerCA, testIssuerURL := newTestIssuer(t)
|
|
testIssuerCABase64 := base64.StdEncoding.EncodeToString([]byte(testIssuerCA))
|
|
testIssuerAuthorizeURL, err := url.Parse("https://example.com/authorize")
|
|
require.NoError(t, err)
|
|
|
|
var (
|
|
testNamespace = "test-namespace"
|
|
testName = "test-name"
|
|
testSecretName = "test-client-secret"
|
|
testAdditionalScopes = []string{"scope1", "scope2", "scope3"}
|
|
testExpectedScopes = []string{"openid", "scope1", "scope2", "scope3"}
|
|
testClientID = "test-oidc-client-id"
|
|
testClientSecret = "test-oidc-client-secret"
|
|
testValidSecretData = map[string][]byte{"clientID": []byte(testClientID), "clientSecret": []byte(testClientSecret)}
|
|
testGroupsClaim = "test-groups-claim"
|
|
testUsernameClaim = "test-username-claim"
|
|
)
|
|
tests := []struct {
|
|
name string
|
|
inputUpstreams []runtime.Object
|
|
inputSecrets []runtime.Object
|
|
wantErr string
|
|
wantLogs []string
|
|
wantResultingCache []provider.UpstreamOIDCIdentityProviderI
|
|
wantResultingUpstreams []v1alpha1.OIDCIdentityProvider
|
|
}{
|
|
{
|
|
name: "no upstreams",
|
|
},
|
|
{
|
|
name: "missing secret",
|
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
|
Issuer: testIssuerURL,
|
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
|
},
|
|
}},
|
|
inputSecrets: []runtime.Object{},
|
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
|
wantLogs: []string{
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="secret \"test-client-secret\" not found" "reason"="SecretNotFound" "status"="False" "type"="ClientCredentialsValid"`,
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`,
|
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="secret \"test-client-secret\" not found" "name"="test-name" "namespace"="test-namespace" "reason"="SecretNotFound" "type"="ClientCredentialsValid"`,
|
|
},
|
|
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
|
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
|
Phase: "Error",
|
|
Conditions: []v1alpha1.Condition{
|
|
{
|
|
Type: "ClientCredentialsValid",
|
|
Status: "False",
|
|
LastTransitionTime: now,
|
|
Reason: "SecretNotFound",
|
|
Message: `secret "test-client-secret" not found`,
|
|
},
|
|
{
|
|
Type: "OIDCDiscoverySucceeded",
|
|
Status: "True",
|
|
LastTransitionTime: now,
|
|
Reason: "Success",
|
|
Message: "discovered issuer configuration",
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "secret has wrong type",
|
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
|
Issuer: testIssuerURL,
|
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
|
},
|
|
}},
|
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
|
Type: "some-other-type",
|
|
Data: testValidSecretData,
|
|
}},
|
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
|
wantLogs: []string{
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")" "reason"="SecretWrongType" "status"="False" "type"="ClientCredentialsValid"`,
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`,
|
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")" "name"="test-name" "namespace"="test-namespace" "reason"="SecretWrongType" "type"="ClientCredentialsValid"`,
|
|
},
|
|
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
|
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
|
Phase: "Error",
|
|
Conditions: []v1alpha1.Condition{
|
|
{
|
|
Type: "ClientCredentialsValid",
|
|
Status: "False",
|
|
LastTransitionTime: now,
|
|
Reason: "SecretWrongType",
|
|
Message: `referenced Secret "test-client-secret" has wrong type "some-other-type" (should be "secrets.pinniped.dev/oidc-client")`,
|
|
},
|
|
{
|
|
Type: "OIDCDiscoverySucceeded",
|
|
Status: "True",
|
|
LastTransitionTime: now,
|
|
Reason: "Success",
|
|
Message: "discovered issuer configuration",
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "secret is missing key",
|
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
|
Issuer: testIssuerURL,
|
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
|
},
|
|
}},
|
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
|
Type: "secrets.pinniped.dev/oidc-client",
|
|
}},
|
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
|
wantLogs: []string{
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]" "reason"="SecretMissingKeys" "status"="False" "type"="ClientCredentialsValid"`,
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`,
|
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]" "name"="test-name" "namespace"="test-namespace" "reason"="SecretMissingKeys" "type"="ClientCredentialsValid"`,
|
|
},
|
|
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
|
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
|
Phase: "Error",
|
|
Conditions: []v1alpha1.Condition{
|
|
{
|
|
Type: "ClientCredentialsValid",
|
|
Status: "False",
|
|
LastTransitionTime: now,
|
|
Reason: "SecretMissingKeys",
|
|
Message: `referenced Secret "test-client-secret" is missing required keys ["clientID" "clientSecret"]`,
|
|
},
|
|
{
|
|
Type: "OIDCDiscoverySucceeded",
|
|
Status: "True",
|
|
LastTransitionTime: now,
|
|
Reason: "Success",
|
|
Message: "discovered issuer configuration",
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "TLS CA bundle is invalid base64",
|
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test-name"},
|
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
|
Issuer: testIssuerURL,
|
|
TLS: &v1alpha1.TLSSpec{
|
|
CertificateAuthorityData: "invalid-base64",
|
|
},
|
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: append(testAdditionalScopes, "xyz", "openid")},
|
|
},
|
|
}},
|
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
|
Type: "secrets.pinniped.dev/oidc-client",
|
|
Data: testValidSecretData,
|
|
}},
|
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
|
wantLogs: []string{
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`,
|
|
},
|
|
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
|
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
|
Phase: "Error",
|
|
Conditions: []v1alpha1.Condition{
|
|
{
|
|
Type: "ClientCredentialsValid",
|
|
Status: "True",
|
|
LastTransitionTime: now,
|
|
Reason: "Success",
|
|
Message: "loaded client credentials",
|
|
},
|
|
{
|
|
Type: "OIDCDiscoverySucceeded",
|
|
Status: "False",
|
|
LastTransitionTime: now,
|
|
Reason: "InvalidTLSConfig",
|
|
Message: `spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7`,
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "TLS CA bundle does not have any certificates",
|
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test-name"},
|
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
|
Issuer: testIssuerURL,
|
|
TLS: &v1alpha1.TLSSpec{
|
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("not-a-pem-ca-bundle")),
|
|
},
|
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: append(testAdditionalScopes, "xyz", "openid")},
|
|
},
|
|
}},
|
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
|
Type: "secrets.pinniped.dev/oidc-client",
|
|
Data: testValidSecretData,
|
|
}},
|
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
|
wantLogs: []string{
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: no certificates found" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="spec.certificateAuthorityData is invalid: no certificates found" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`,
|
|
},
|
|
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
|
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
|
Phase: "Error",
|
|
Conditions: []v1alpha1.Condition{
|
|
{
|
|
Type: "ClientCredentialsValid",
|
|
Status: "True",
|
|
LastTransitionTime: now,
|
|
Reason: "Success",
|
|
Message: "loaded client credentials",
|
|
},
|
|
{
|
|
Type: "OIDCDiscoverySucceeded",
|
|
Status: "False",
|
|
LastTransitionTime: now,
|
|
Reason: "InvalidTLSConfig",
|
|
Message: `spec.certificateAuthorityData is invalid: no certificates found`,
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "issuer is invalid URL",
|
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
|
Issuer: "invalid-url-that-is-really-really-long",
|
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
|
},
|
|
}},
|
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
|
Type: "secrets.pinniped.dev/oidc-client",
|
|
Data: testValidSecretData,
|
|
}},
|
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
|
wantLogs: []string{
|
|
`oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="Get \"invalid-url-that-is-really-really-long/.well-known/openid-configuration\": unsupported protocol scheme \"\"" "issuer"="invalid-url-that-is-really-really-long" "name"="test-name" "namespace"="test-namespace"`,
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"invalid-url-that-is-really-really-long\":\nGet \"invalid-url-that-is-really-really-long/.well-known/openid-configuration\": unsupported protocol [truncated 9 chars]" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"invalid-url-that-is-really-really-long\":\nGet \"invalid-url-that-is-really-really-long/.well-known/openid-configuration\": unsupported protocol [truncated 9 chars]" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`,
|
|
},
|
|
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
|
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
|
Phase: "Error",
|
|
Conditions: []v1alpha1.Condition{
|
|
{
|
|
Type: "ClientCredentialsValid",
|
|
Status: "True",
|
|
LastTransitionTime: now,
|
|
Reason: "Success",
|
|
Message: "loaded client credentials",
|
|
},
|
|
{
|
|
Type: "OIDCDiscoverySucceeded",
|
|
Status: "False",
|
|
LastTransitionTime: now,
|
|
Reason: "Unreachable",
|
|
Message: `failed to perform OIDC discovery against "invalid-url-that-is-really-really-long":
|
|
Get "invalid-url-that-is-really-really-long/.well-known/openid-configuration": unsupported protocol [truncated 9 chars]`,
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "issuer returns invalid authorize URL",
|
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
|
Issuer: testIssuerURL + "/invalid",
|
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
|
},
|
|
}},
|
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
|
Type: "secrets.pinniped.dev/oidc-client",
|
|
Data: testValidSecretData,
|
|
}},
|
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
|
wantLogs: []string{
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to parse authorization endpoint URL: parse \"%\": invalid URL escape \"%\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to parse authorization endpoint URL: parse \"%\": invalid URL escape \"%\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`,
|
|
},
|
|
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
|
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
|
Phase: "Error",
|
|
Conditions: []v1alpha1.Condition{
|
|
{
|
|
Type: "ClientCredentialsValid",
|
|
Status: "True",
|
|
LastTransitionTime: now,
|
|
Reason: "Success",
|
|
Message: "loaded client credentials",
|
|
},
|
|
{
|
|
Type: "OIDCDiscoverySucceeded",
|
|
Status: "False",
|
|
LastTransitionTime: now,
|
|
Reason: "InvalidResponse",
|
|
Message: `failed to parse authorization endpoint URL: parse "%": invalid URL escape "%"`,
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "issuer returns insecure authorize URL",
|
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
|
Issuer: testIssuerURL + "/insecure",
|
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
|
},
|
|
}},
|
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
|
Type: "secrets.pinniped.dev/oidc-client",
|
|
Data: testValidSecretData,
|
|
}},
|
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
|
wantLogs: []string{
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="authorization endpoint URL scheme must be \"https\", not \"http\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="authorization endpoint URL scheme must be \"https\", not \"http\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`,
|
|
},
|
|
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
|
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
|
Phase: "Error",
|
|
Conditions: []v1alpha1.Condition{
|
|
{
|
|
Type: "ClientCredentialsValid",
|
|
Status: "True",
|
|
LastTransitionTime: now,
|
|
Reason: "Success",
|
|
Message: "loaded client credentials",
|
|
},
|
|
{
|
|
Type: "OIDCDiscoverySucceeded",
|
|
Status: "False",
|
|
LastTransitionTime: now,
|
|
Reason: "InvalidResponse",
|
|
Message: `authorization endpoint URL scheme must be "https", not "http"`,
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "upstream with error becomes valid",
|
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test-name"},
|
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
|
Issuer: testIssuerURL,
|
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{
|
|
AdditionalScopes: append(testAdditionalScopes, "xyz", "openid"),
|
|
AllowPasswordGrant: true,
|
|
},
|
|
Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim},
|
|
},
|
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
|
Phase: "Error",
|
|
Conditions: []v1alpha1.Condition{
|
|
{Type: "ClientCredentialsValid", Status: "False", LastTransitionTime: earlier, Reason: "SomeError1", Message: "some previous error 1"},
|
|
{Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: earlier, Reason: "SomeError2", Message: "some previous error 2"},
|
|
},
|
|
},
|
|
}},
|
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
|
Type: "secrets.pinniped.dev/oidc-client",
|
|
Data: testValidSecretData,
|
|
}},
|
|
wantLogs: []string{
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`,
|
|
},
|
|
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{
|
|
&oidctestutil.TestUpstreamOIDCIdentityProvider{
|
|
Name: testName,
|
|
ClientID: testClientID,
|
|
AuthorizationURL: *testIssuerAuthorizeURL,
|
|
Scopes: append(testExpectedScopes, "xyz"),
|
|
UsernameClaim: testUsernameClaim,
|
|
GroupsClaim: testGroupsClaim,
|
|
AllowPasswordGrant: true,
|
|
},
|
|
},
|
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
|
Phase: "Ready",
|
|
Conditions: []v1alpha1.Condition{
|
|
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "loaded client credentials"},
|
|
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "discovered issuer configuration"},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "existing valid upstream",
|
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
|
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
|
Issuer: testIssuerURL,
|
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{
|
|
AdditionalScopes: testAdditionalScopes,
|
|
AllowPasswordGrant: false,
|
|
},
|
|
Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim},
|
|
},
|
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
|
Phase: "Ready",
|
|
Conditions: []v1alpha1.Condition{
|
|
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"},
|
|
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"},
|
|
},
|
|
},
|
|
}},
|
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
|
Type: "secrets.pinniped.dev/oidc-client",
|
|
Data: testValidSecretData,
|
|
}},
|
|
wantLogs: []string{
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`,
|
|
},
|
|
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{
|
|
&oidctestutil.TestUpstreamOIDCIdentityProvider{
|
|
Name: testName,
|
|
ClientID: testClientID,
|
|
AuthorizationURL: *testIssuerAuthorizeURL,
|
|
Scopes: testExpectedScopes,
|
|
UsernameClaim: testUsernameClaim,
|
|
GroupsClaim: testGroupsClaim,
|
|
AllowPasswordGrant: false,
|
|
},
|
|
},
|
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
|
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
|
Phase: "Ready",
|
|
Conditions: []v1alpha1.Condition{
|
|
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234},
|
|
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "existing valid upstream with trailing slash",
|
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
|
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
|
Issuer: testIssuerURL + "/ends-with-slash/",
|
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
|
Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim},
|
|
},
|
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
|
Phase: "Ready",
|
|
Conditions: []v1alpha1.Condition{
|
|
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"},
|
|
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"},
|
|
},
|
|
},
|
|
}},
|
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
|
Type: "secrets.pinniped.dev/oidc-client",
|
|
Data: testValidSecretData,
|
|
}},
|
|
wantLogs: []string{
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`,
|
|
},
|
|
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{
|
|
&oidctestutil.TestUpstreamOIDCIdentityProvider{
|
|
Name: testName,
|
|
ClientID: testClientID,
|
|
AuthorizationURL: *testIssuerAuthorizeURL,
|
|
Scopes: testExpectedScopes,
|
|
UsernameClaim: testUsernameClaim,
|
|
GroupsClaim: testGroupsClaim,
|
|
AllowPasswordGrant: false,
|
|
},
|
|
},
|
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
|
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
|
Phase: "Ready",
|
|
Conditions: []v1alpha1.Condition{
|
|
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234},
|
|
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "issuer is invalid URL, missing trailing slash",
|
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
|
Issuer: testIssuerURL + "/ends-with-slash",
|
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
|
},
|
|
}},
|
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
|
Type: "secrets.pinniped.dev/oidc-client",
|
|
Data: testValidSecretData,
|
|
}},
|
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
|
wantLogs: []string{
|
|
`oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="oidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "issuer"="` + testIssuerURL + `/ends-with-slash" "name"="test-name" "namespace"="test-namespace"`,
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/ends-with-slash\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/ends-with-slash\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`,
|
|
},
|
|
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
|
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
|
Phase: "Error",
|
|
Conditions: []v1alpha1.Condition{
|
|
{
|
|
Type: "ClientCredentialsValid",
|
|
Status: "True",
|
|
LastTransitionTime: now,
|
|
Reason: "Success",
|
|
Message: "loaded client credentials",
|
|
},
|
|
{
|
|
Type: "OIDCDiscoverySucceeded",
|
|
Status: "False",
|
|
LastTransitionTime: now,
|
|
Reason: "Unreachable",
|
|
Message: `failed to perform OIDC discovery against "` + testIssuerURL + `/ends-with-slash":
|
|
oidc: issuer did not match the issuer returned by provider, expected "` + testIssuerURL + `/ends-with-slash" got "` + testIssuerURL + `/ends-with-slash/"`,
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "issuer is invalid URL, extra trailing slash",
|
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
|
Issuer: testIssuerURL + "/",
|
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
|
},
|
|
}},
|
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
|
Type: "secrets.pinniped.dev/oidc-client",
|
|
Data: testValidSecretData,
|
|
}},
|
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
|
wantLogs: []string{
|
|
`oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="oidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "issuer"="` + testIssuerURL + `/" "name"="test-name" "namespace"="test-namespace"`,
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`,
|
|
},
|
|
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
|
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
|
Phase: "Error",
|
|
Conditions: []v1alpha1.Condition{
|
|
{
|
|
Type: "ClientCredentialsValid",
|
|
Status: "True",
|
|
LastTransitionTime: now,
|
|
Reason: "Success",
|
|
Message: "loaded client credentials",
|
|
},
|
|
{
|
|
Type: "OIDCDiscoverySucceeded",
|
|
Status: "False",
|
|
LastTransitionTime: now,
|
|
Reason: "Unreachable",
|
|
Message: `failed to perform OIDC discovery against "` + testIssuerURL + `/":
|
|
oidc: issuer did not match the issuer returned by provider, expected "` + testIssuerURL + `/" got "` + testIssuerURL + `"`,
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
fakePinnipedClient := pinnipedfake.NewSimpleClientset(tt.inputUpstreams...)
|
|
pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0)
|
|
fakeKubeClient := fake.NewSimpleClientset(tt.inputSecrets...)
|
|
kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0)
|
|
testLog := testlogger.New(t)
|
|
cache := provider.NewDynamicUpstreamIDPProvider()
|
|
cache.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{
|
|
&upstreamoidc.ProviderConfig{Name: "initial-entry"},
|
|
})
|
|
|
|
controller := New(
|
|
cache,
|
|
fakePinnipedClient,
|
|
pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(),
|
|
kubeInformers.Core().V1().Secrets(),
|
|
testLog,
|
|
controllerlib.WithInformer,
|
|
)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
pinnipedInformers.Start(ctx.Done())
|
|
kubeInformers.Start(ctx.Done())
|
|
controllerlib.TestRunSynchronously(t, controller)
|
|
|
|
syncCtx := controllerlib.Context{Context: ctx, Key: controllerlib.Key{}}
|
|
|
|
if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" {
|
|
require.EqualError(t, err, tt.wantErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
require.Equal(t, strings.Join(tt.wantLogs, "\n"), strings.Join(testLog.Lines(), "\n"))
|
|
|
|
actualIDPList := cache.GetOIDCIdentityProviders()
|
|
require.Equal(t, len(tt.wantResultingCache), len(actualIDPList))
|
|
for i := range actualIDPList {
|
|
actualIDP := actualIDPList[i].(*upstreamoidc.ProviderConfig)
|
|
require.Equal(t, tt.wantResultingCache[i].GetName(), actualIDP.GetName())
|
|
require.Equal(t, tt.wantResultingCache[i].GetClientID(), actualIDP.GetClientID())
|
|
require.Equal(t, tt.wantResultingCache[i].GetAuthorizationURL().String(), actualIDP.GetAuthorizationURL().String())
|
|
require.Equal(t, tt.wantResultingCache[i].GetUsernameClaim(), actualIDP.GetUsernameClaim())
|
|
require.Equal(t, tt.wantResultingCache[i].GetGroupsClaim(), actualIDP.GetGroupsClaim())
|
|
require.Equal(t, tt.wantResultingCache[i].AllowsPasswordGrant(), actualIDP.AllowsPasswordGrant())
|
|
require.ElementsMatch(t, tt.wantResultingCache[i].GetScopes(), actualIDP.GetScopes())
|
|
|
|
// We always want to use the proxy from env on these clients, so although the following assertions
|
|
// are a little hacky, this is a cheap way to test that we are using it.
|
|
actualTransport, ok := actualIDP.Client.Transport.(*http.Transport)
|
|
require.True(t, ok, "expected cached provider to have client with Transport of type *http.Transport")
|
|
httpProxyFromEnvFunction := reflect.ValueOf(http.ProxyFromEnvironment).Pointer()
|
|
actualTransportProxyFunction := reflect.ValueOf(actualTransport.Proxy).Pointer()
|
|
require.Equal(t, httpProxyFromEnvFunction, actualTransportProxyFunction,
|
|
"Transport should have used http.ProxyFromEnvironment as its Proxy func")
|
|
// We also want a reasonable timeout on each request/response cycle for OIDC discovery and JWKS.
|
|
require.Equal(t, time.Minute, actualIDP.Client.Timeout)
|
|
}
|
|
|
|
actualUpstreams, err := fakePinnipedClient.IDPV1alpha1().OIDCIdentityProviders(testNamespace).List(ctx, metav1.ListOptions{})
|
|
require.NoError(t, err)
|
|
|
|
// Assert on the expected Status of the upstreams. Preprocess the upstreams a bit so that they're easier to assert against.
|
|
require.ElementsMatch(t, tt.wantResultingUpstreams, normalizeOIDCUpstreams(actualUpstreams.Items, now))
|
|
|
|
// Running the sync() a second time should be idempotent except for logs, and should return the same error.
|
|
// This also helps exercise code paths where the OIDC provider discovery hits cache.
|
|
if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" {
|
|
require.EqualError(t, err, tt.wantErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func normalizeOIDCUpstreams(upstreams []v1alpha1.OIDCIdentityProvider, now metav1.Time) []v1alpha1.OIDCIdentityProvider {
|
|
result := make([]v1alpha1.OIDCIdentityProvider, 0, len(upstreams))
|
|
for _, u := range upstreams {
|
|
normalized := u.DeepCopy()
|
|
|
|
// We're only interested in comparing the status, so zero out the spec.
|
|
normalized.Spec = v1alpha1.OIDCIdentityProviderSpec{}
|
|
|
|
// Round down the LastTransitionTime values to `now` if they were just updated. This makes
|
|
// it much easier to encode assertions about the expected timestamps.
|
|
for i := range normalized.Status.Conditions {
|
|
if time.Since(normalized.Status.Conditions[i].LastTransitionTime.Time) < 5*time.Second {
|
|
normalized.Status.Conditions[i].LastTransitionTime = now
|
|
}
|
|
}
|
|
result = append(result, *normalized)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func newTestIssuer(t *testing.T) (string, string) {
|
|
mux := http.NewServeMux()
|
|
caBundlePEM, testURL := testutil.TLSTestServer(t, mux.ServeHTTP)
|
|
|
|
type providerJSON struct {
|
|
Issuer string `json:"issuer"`
|
|
AuthURL string `json:"authorization_endpoint"`
|
|
TokenURL string `json:"token_endpoint"`
|
|
JWKSURL string `json:"jwks_uri"`
|
|
}
|
|
|
|
// At the root of the server, serve an issuer with a valid discovery response.
|
|
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("content-type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(&providerJSON{
|
|
Issuer: testURL,
|
|
AuthURL: "https://example.com/authorize",
|
|
})
|
|
})
|
|
|
|
// At "/invalid", serve an issuer that returns an invalid authorization URL (not parseable).
|
|
mux.HandleFunc("/invalid/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("content-type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(&providerJSON{
|
|
Issuer: testURL + "/invalid",
|
|
AuthURL: "%",
|
|
})
|
|
})
|
|
|
|
// At "/insecure", serve an issuer that returns an insecure authorization URL (not https://).
|
|
mux.HandleFunc("/insecure/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("content-type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(&providerJSON{
|
|
Issuer: testURL + "/insecure",
|
|
AuthURL: "http://example.com/authorize",
|
|
})
|
|
})
|
|
|
|
// handle the four issuer with trailing slash configs
|
|
|
|
// valid case in= out=
|
|
// handled above at the root of testURL
|
|
|
|
// valid case in=/ out=/
|
|
mux.HandleFunc("/ends-with-slash/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("content-type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(&providerJSON{
|
|
Issuer: testURL + "/ends-with-slash/",
|
|
AuthURL: "https://example.com/authorize",
|
|
})
|
|
})
|
|
|
|
// invalid case in= out=/
|
|
// can be tested using /ends-with-slash/ endpoint
|
|
|
|
// invalid case in=/ out=
|
|
// can be tested using root endpoint
|
|
|
|
return caBundlePEM, testURL
|
|
}
|