2020-10-06 19:20:29 +00:00
|
|
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
package integration
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2020-10-07 00:53:29 +00:00
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
2020-10-06 19:20:29 +00:00
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
2020-10-07 00:53:29 +00:00
|
|
|
"github.com/stretchr/testify/assert"
|
2020-10-06 19:20:29 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
2020-10-08 02:18:34 +00:00
|
|
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
2020-10-06 19:20:29 +00:00
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
2020-10-07 00:53:29 +00:00
|
|
|
|
|
|
|
"go.pinniped.dev/generated/1.19/apis/config/v1alpha1"
|
2020-10-08 02:18:34 +00:00
|
|
|
pinnipedclientset "go.pinniped.dev/generated/1.19/client/clientset/versioned"
|
2020-10-07 00:53:29 +00:00
|
|
|
"go.pinniped.dev/internal/here"
|
|
|
|
"go.pinniped.dev/test/library"
|
2020-10-06 19:20:29 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func TestSupervisorOIDCDiscovery(t *testing.T) {
|
|
|
|
env := library.IntegrationEnv(t)
|
|
|
|
client := library.NewPinnipedClientset(t)
|
|
|
|
|
2020-10-07 00:53:29 +00:00
|
|
|
ns := env.SupervisorNamespace
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
2020-10-06 19:20:29 +00:00
|
|
|
defer cancel()
|
|
|
|
|
2020-10-07 00:53:29 +00:00
|
|
|
// Temporarily remove any existing OIDCProviderConfigs from the cluster so we can test from a clean slate.
|
|
|
|
originalConfigList, err := client.ConfigV1alpha1().OIDCProviderConfigs(ns).List(ctx, metav1.ListOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
for _, config := range originalConfigList.Items {
|
|
|
|
err := client.ConfigV1alpha1().OIDCProviderConfigs(ns).Delete(ctx, config.Name, metav1.DeleteOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// When this test has finished, recreate any OIDCProviderConfigs that had existed on the cluster before this test.
|
|
|
|
t.Cleanup(func() {
|
2020-10-07 14:53:05 +00:00
|
|
|
cleanupCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
|
|
defer cancel()
|
|
|
|
|
2020-10-07 00:53:29 +00:00
|
|
|
for _, config := range originalConfigList.Items {
|
|
|
|
thisConfig := config
|
2020-10-07 14:53:05 +00:00
|
|
|
thisConfig.ResourceVersion = "" // Get rid of resource version since we can't create an object with one.
|
|
|
|
_, err := client.ConfigV1alpha1().OIDCProviderConfigs(ns).Create(cleanupCtx, &thisConfig, metav1.CreateOptions{})
|
2020-10-07 00:53:29 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// Test that there is no default discovery endpoint available when there are no OIDCProviderConfigs.
|
2020-10-08 02:18:34 +00:00
|
|
|
requireDiscoveryEndpointIsNotFound(t, fmt.Sprintf("http://%s", env.SupervisorAddress))
|
|
|
|
|
|
|
|
// Define several unique issuer strings.
|
|
|
|
issuer1 := fmt.Sprintf("http://%s/nested/issuer1", env.SupervisorAddress)
|
|
|
|
issuer2 := fmt.Sprintf("http://%s/nested/issuer2", env.SupervisorAddress)
|
|
|
|
issuer3 := fmt.Sprintf("http://%s/issuer3", env.SupervisorAddress)
|
|
|
|
issuer4 := fmt.Sprintf("http://%s/issuer4", env.SupervisorAddress)
|
2020-10-08 17:27:45 +00:00
|
|
|
issuer5 := fmt.Sprintf("http://%s/issuer5", env.SupervisorAddress)
|
|
|
|
badIssuer := fmt.Sprintf("http://%s/badIssuer?cannot-use=queries", env.SupervisorAddress)
|
2020-10-08 02:18:34 +00:00
|
|
|
|
|
|
|
// When OIDCProviderConfig are created in sequence they each cause a discovery endpoint to appear only for as long as the OIDCProviderConfig exists.
|
2020-10-08 17:27:45 +00:00
|
|
|
config1 := requireCreatingOIDCProviderConfigCausesWellKnownEndpointToAppear(t, client, ns, issuer1, "from-integration-test1")
|
|
|
|
requireDeletingOIDCProviderConfigCausesWellKnownEndpointToDisappear(t, config1, client, ns, issuer1)
|
|
|
|
config2 := requireCreatingOIDCProviderConfigCausesWellKnownEndpointToAppear(t, client, ns, issuer2, "from-integration-test2")
|
|
|
|
requireDeletingOIDCProviderConfigCausesWellKnownEndpointToDisappear(t, config2, client, ns, issuer2)
|
2020-10-08 02:18:34 +00:00
|
|
|
|
|
|
|
// When multiple OIDCProviderConfigs exist at the same time they each serve a unique discovery endpoint.
|
2020-10-08 17:27:45 +00:00
|
|
|
config3 := requireCreatingOIDCProviderConfigCausesWellKnownEndpointToAppear(t, client, ns, issuer3, "from-integration-test3")
|
|
|
|
config4 := requireCreatingOIDCProviderConfigCausesWellKnownEndpointToAppear(t, client, ns, issuer4, "from-integration-test4")
|
2020-10-08 02:18:34 +00:00
|
|
|
requireWellKnownEndpointIsWorking(t, issuer3) // discovery for issuer3 is still working after issuer4 started working
|
|
|
|
|
|
|
|
// When they are deleted they stop serving discovery endpoints.
|
2020-10-08 17:27:45 +00:00
|
|
|
requireDeletingOIDCProviderConfigCausesWellKnownEndpointToDisappear(t, config3, client, ns, issuer3)
|
|
|
|
requireDeletingOIDCProviderConfigCausesWellKnownEndpointToDisappear(t, config4, client, ns, issuer4)
|
|
|
|
|
|
|
|
// When the same issuer is added twice, both issuers are marked as duplicates, and neither provider is serving.
|
|
|
|
config5Duplicate1 := requireCreatingOIDCProviderConfigCausesWellKnownEndpointToAppear(t, client, ns, issuer5, "from-integration-test5")
|
|
|
|
config5Duplicate2 := createOIDCProviderConfig(t, "from-integration-test5-duplicate", client, ns, issuer5)
|
|
|
|
requireStatus(t, client, ns, config5Duplicate1.Name, v1alpha1.DuplicateOIDCProviderStatus)
|
|
|
|
requireStatus(t, client, ns, config5Duplicate2.Name, v1alpha1.DuplicateOIDCProviderStatus)
|
|
|
|
requireDiscoveryEndpointIsNotFound(t, issuer5)
|
|
|
|
|
|
|
|
// If we delete the first duplicate issuer, the second duplicate issuer starts serving.
|
|
|
|
requireDelete(t, client, ns, config5Duplicate1.Name)
|
|
|
|
requireWellKnownEndpointIsWorking(t, issuer5)
|
|
|
|
requireStatus(t, client, ns, config5Duplicate2.Name, v1alpha1.SuccessOIDCProviderStatus)
|
|
|
|
|
|
|
|
// When we finally delete all issuers, the endpoint should be down.
|
|
|
|
requireDeletingOIDCProviderConfigCausesWellKnownEndpointToDisappear(t, config5Duplicate2, client, ns, issuer5)
|
|
|
|
|
|
|
|
// When we create a provider with an invalid issuer, the status is set to invalid.
|
|
|
|
badConfig := createOIDCProviderConfig(t, "from-integration-test6", client, ns, badIssuer)
|
|
|
|
requireStatus(t, client, ns, badConfig.Name, v1alpha1.InvalidOIDCProviderStatus)
|
|
|
|
requireDiscoveryEndpointIsNotFound(t, badIssuer)
|
2020-10-08 02:18:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func requireDiscoveryEndpointIsNotFound(t *testing.T, issuerName string) {
|
|
|
|
t.Helper()
|
|
|
|
httpClient := &http.Client{}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
|
|
defer cancel()
|
|
|
|
|
2020-10-07 00:53:29 +00:00
|
|
|
requestNonExistentPath, err := http.NewRequestWithContext(
|
|
|
|
ctx,
|
|
|
|
http.MethodGet,
|
2020-10-08 02:18:34 +00:00
|
|
|
fmt.Sprintf("%s/.well-known/openid-configuration", issuerName),
|
2020-10-07 00:53:29 +00:00
|
|
|
nil,
|
|
|
|
)
|
2020-10-08 02:18:34 +00:00
|
|
|
|
|
|
|
var response *http.Response
|
|
|
|
assert.Eventually(t, func() bool {
|
|
|
|
response, err = httpClient.Do(requestNonExistentPath) //nolint:bodyclose
|
|
|
|
return err == nil && response.StatusCode == http.StatusNotFound
|
|
|
|
}, 10*time.Second, 200*time.Millisecond)
|
2020-10-07 00:53:29 +00:00
|
|
|
require.NoError(t, err)
|
2020-10-08 02:18:34 +00:00
|
|
|
require.Equal(t, http.StatusNotFound, response.StatusCode)
|
|
|
|
err = response.Body.Close()
|
2020-10-07 00:53:29 +00:00
|
|
|
require.NoError(t, err)
|
2020-10-08 02:18:34 +00:00
|
|
|
}
|
2020-10-07 00:53:29 +00:00
|
|
|
|
2020-10-08 02:18:34 +00:00
|
|
|
func requireCreatingOIDCProviderConfigCausesWellKnownEndpointToAppear(t *testing.T, client pinnipedclientset.Interface, ns string, issuerName string, oidcProviderConfigName string) *v1alpha1.OIDCProviderConfig {
|
|
|
|
t.Helper()
|
|
|
|
newOIDCProviderConfig := createOIDCProviderConfig(t, oidcProviderConfigName, client, ns, issuerName)
|
|
|
|
requireWellKnownEndpointIsWorking(t, issuerName)
|
2020-10-08 17:27:45 +00:00
|
|
|
requireStatus(t, client, ns, oidcProviderConfigName, v1alpha1.SuccessOIDCProviderStatus)
|
2020-10-08 02:18:34 +00:00
|
|
|
return newOIDCProviderConfig
|
|
|
|
}
|
|
|
|
|
|
|
|
func requireDeletingOIDCProviderConfigCausesWellKnownEndpointToDisappear(t *testing.T, existingOIDCProviderConfig *v1alpha1.OIDCProviderConfig, client pinnipedclientset.Interface, ns string, issuerName string) {
|
|
|
|
t.Helper()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
// Delete the OIDCProviderConfig.
|
|
|
|
err := client.ConfigV1alpha1().OIDCProviderConfigs(ns).Delete(ctx, existingOIDCProviderConfig.Name, metav1.DeleteOptions{})
|
2020-10-07 00:53:29 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2020-10-08 02:18:34 +00:00
|
|
|
// Fetch that same discovery endpoint as before, but now it should not exist anymore. Give it some time for the endpoint to go away.
|
|
|
|
requireDiscoveryEndpointIsNotFound(t, issuerName)
|
|
|
|
}
|
2020-10-07 14:53:05 +00:00
|
|
|
|
2020-10-08 02:18:34 +00:00
|
|
|
func requireWellKnownEndpointIsWorking(t *testing.T, issuerName string) {
|
|
|
|
t.Helper()
|
|
|
|
httpClient := &http.Client{}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
|
|
defer cancel()
|
2020-10-07 00:53:29 +00:00
|
|
|
|
2020-10-08 02:18:34 +00:00
|
|
|
// Define a request to the new discovery endpoint which should have been created by an OIDCProviderConfig.
|
2020-10-07 00:53:29 +00:00
|
|
|
requestDiscoveryEndpoint, err := http.NewRequestWithContext(
|
|
|
|
ctx,
|
|
|
|
http.MethodGet,
|
2020-10-08 02:18:34 +00:00
|
|
|
fmt.Sprintf("%s/.well-known/openid-configuration", issuerName),
|
2020-10-07 00:53:29 +00:00
|
|
|
nil,
|
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Fetch that discovery endpoint. Give it some time for the endpoint to come into existence.
|
|
|
|
var response *http.Response
|
|
|
|
assert.Eventually(t, func() bool {
|
2020-10-08 02:18:34 +00:00
|
|
|
response, err = httpClient.Do(requestDiscoveryEndpoint) //nolint:bodyclose
|
2020-10-07 14:53:05 +00:00
|
|
|
return err == nil && response.StatusCode == http.StatusOK
|
2020-10-07 00:53:29 +00:00
|
|
|
}, 10*time.Second, 200*time.Millisecond)
|
|
|
|
require.NoError(t, err)
|
2020-10-07 14:53:05 +00:00
|
|
|
require.Equal(t, http.StatusOK, response.StatusCode)
|
|
|
|
|
2020-10-07 00:53:29 +00:00
|
|
|
responseBody, err := ioutil.ReadAll(response.Body)
|
|
|
|
require.NoError(t, err)
|
|
|
|
err = response.Body.Close()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Check that the response matches our expectations.
|
|
|
|
expectedResultTemplate := here.Doc(`{
|
|
|
|
"issuer": "%s",
|
2020-10-08 18:28:21 +00:00
|
|
|
"authorization_endpoint": "%s/oauth2/authorize",
|
|
|
|
"token_endpoint": "%s/oauth2/token",
|
2020-10-07 00:53:29 +00:00
|
|
|
"token_endpoint_auth_methods_supported": ["client_secret_basic"],
|
|
|
|
"token_endpoint_auth_signing_alg_values_supported": ["RS256"],
|
|
|
|
"jwks_uri": "%s/jwks.json",
|
|
|
|
"scopes_supported": ["openid", "offline"],
|
|
|
|
"response_types_supported": ["code"],
|
|
|
|
"claims_supported": ["groups"],
|
2020-10-07 15:33:50 +00:00
|
|
|
"subject_types_supported": ["public"],
|
|
|
|
"id_token_signing_alg_values_supported": ["RS256"]
|
2020-10-07 00:53:29 +00:00
|
|
|
}`)
|
2020-10-08 02:18:34 +00:00
|
|
|
expectedJSON := fmt.Sprintf(expectedResultTemplate, issuerName, issuerName, issuerName, issuerName)
|
2020-10-06 19:20:29 +00:00
|
|
|
|
2020-10-07 00:53:29 +00:00
|
|
|
require.Equal(t, "application/json", response.Header.Get("content-type"))
|
|
|
|
require.JSONEq(t, expectedJSON, string(responseBody))
|
2020-10-06 19:20:29 +00:00
|
|
|
}
|
2020-10-08 02:18:34 +00:00
|
|
|
|
|
|
|
func createOIDCProviderConfig(t *testing.T, oidcProviderConfigName string, client pinnipedclientset.Interface, ns string, issuerName string) *v1alpha1.OIDCProviderConfig {
|
|
|
|
t.Helper()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
newOIDCProviderConfig := v1alpha1.OIDCProviderConfig{
|
|
|
|
TypeMeta: metav1.TypeMeta{
|
|
|
|
Kind: "OIDCProviderConfig",
|
|
|
|
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
|
|
|
},
|
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
Name: oidcProviderConfigName,
|
|
|
|
Namespace: ns,
|
|
|
|
},
|
|
|
|
Spec: v1alpha1.OIDCProviderConfigSpec{
|
|
|
|
Issuer: issuerName,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
createdOIDCProviderConfig, err := client.ConfigV1alpha1().OIDCProviderConfigs(ns).Create(ctx, &newOIDCProviderConfig, metav1.CreateOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// When this test has finished, be sure to clean up the new OIDCProviderConfig.
|
|
|
|
t.Cleanup(func() {
|
|
|
|
cleanupCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
err = client.ConfigV1alpha1().OIDCProviderConfigs(ns).Delete(cleanupCtx, newOIDCProviderConfig.Name, metav1.DeleteOptions{})
|
|
|
|
notFound := k8serrors.IsNotFound(err)
|
|
|
|
// It's okay if it is not found, because it might have been deleted by another part of this test.
|
|
|
|
if !notFound {
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return createdOIDCProviderConfig
|
|
|
|
}
|
2020-10-08 17:27:45 +00:00
|
|
|
|
|
|
|
func requireDelete(t *testing.T, client pinnipedclientset.Interface, ns, name string) {
|
|
|
|
t.Helper()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
err := client.ConfigV1alpha1().OIDCProviderConfigs(ns).Delete(ctx, name, metav1.DeleteOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
func requireStatus(t *testing.T, client pinnipedclientset.Interface, ns, name string, status v1alpha1.OIDCProviderStatus) {
|
|
|
|
t.Helper()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
var opc *v1alpha1.OIDCProviderConfig
|
|
|
|
var err error
|
|
|
|
assert.Eventually(t, func() bool {
|
|
|
|
opc, err = client.ConfigV1alpha1().OIDCProviderConfigs(ns).Get(ctx, name, metav1.GetOptions{})
|
|
|
|
return err == nil && opc.Status.Status == status
|
|
|
|
}, 10*time.Second, 200*time.Millisecond)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equalf(t, status, opc.Status.Status, "unexpected status (message = '%s')", opc.Status.Message)
|
|
|
|
}
|