2022-03-08 20:28:09 +00:00
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
2020-10-06 19:20:29 +00:00
// SPDX-License-Identifier: Apache-2.0
package integration
import (
"context"
2020-10-20 23:46:33 +00:00
"crypto/tls"
"crypto/x509"
2020-10-19 19:21:18 +00:00
"encoding/json"
2020-10-07 00:53:29 +00:00
"fmt"
2021-12-15 20:48:55 +00:00
"io"
2020-10-27 21:57:25 +00:00
"net"
2020-10-07 00:53:29 +00:00
"net/http"
2020-10-20 21:00:36 +00:00
"net/url"
"strings"
2020-10-06 19:20:29 +00:00
"testing"
"time"
"github.com/stretchr/testify/require"
2020-10-27 21:57:25 +00:00
corev1 "k8s.io/api/core/v1"
2020-10-28 19:49:41 +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-27 21:57:25 +00:00
"k8s.io/client-go/kubernetes"
2021-03-11 19:18:15 +00:00
"k8s.io/client-go/util/retry"
2020-10-07 00:53:29 +00:00
2021-02-16 19:00:08 +00:00
"go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
2020-10-27 21:57:25 +00:00
"go.pinniped.dev/internal/certauthority"
2022-03-29 23:58:41 +00:00
"go.pinniped.dev/internal/crypto/ptls"
2020-10-07 00:53:29 +00:00
"go.pinniped.dev/internal/here"
2021-06-22 15:23:19 +00:00
"go.pinniped.dev/test/testlib"
2020-10-06 19:20:29 +00:00
)
2022-03-10 20:26:22 +00:00
// TestSupervisorOIDCDiscovery_Disruptive is intended to exercise the supervisor's HTTPS port.
// It can either access it directly via the env.SupervisorHTTPSAddress setting, or it can access
// it indirectly through a TLS-enabled Ingress which uses the supervisor's HTTPS port as its backend
// (via the env.SupervisorHTTPSIngressAddress and env.SupervisorHTTPSIngressCABundle settings),
// or it can exercise it both ways when all of those env settings are present.
2021-08-26 18:31:50 +00:00
// Never run this test in parallel since deleting all federation domains is disruptive, see main_test.go.
func TestSupervisorOIDCDiscovery_Disruptive ( t * testing . T ) {
2021-06-22 15:23:19 +00:00
env := testlib . IntegrationEnv ( t )
client := testlib . NewSupervisorClientset ( t )
2022-03-10 20:26:22 +00:00
kubeClient := testlib . NewKubernetesClientset ( t )
2020-10-29 22:42:22 +00:00
ns := env . SupervisorNamespace
2021-01-12 20:55:31 +00:00
2020-12-16 23:13:02 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 10 * time . Minute )
2020-10-29 22:42:22 +00:00
defer cancel ( )
2022-03-10 20:26:22 +00:00
httpsAddress := env . SupervisorHTTPSAddress
var ips [ ] net . IP
if host , _ , err := net . SplitHostPort ( httpsAddress ) ; err == nil {
httpsAddress = host
}
if ip := net . ParseIP ( httpsAddress ) ; ip != nil {
ips = append ( ips , ip )
}
2021-06-22 15:23:19 +00:00
temporarilyRemoveAllFederationDomainsAndDefaultTLSCertSecret ( ctx , t , ns , defaultTLSCertSecretName ( env ) , client , testlib . NewKubernetesClientset ( t ) )
2022-03-10 20:26:22 +00:00
defaultCA := createTLSCertificateSecret ( ctx , t , ns , httpsAddress , ips , defaultTLSCertSecretName ( env ) , kubeClient )
2020-10-29 22:42:22 +00:00
tests := [ ] struct {
2022-03-10 20:26:22 +00:00
Name string
2020-10-29 22:42:22 +00:00
Scheme string
Address string
CABundle string
} {
2022-03-10 20:26:22 +00:00
{ Name : "direct https" , Scheme : "https" , Address : env . SupervisorHTTPSAddress , CABundle : string ( defaultCA . Bundle ( ) ) } ,
{ Name : "ingress https" , Scheme : "https" , Address : env . SupervisorHTTPSIngressAddress , CABundle : env . SupervisorHTTPSIngressCABundle } ,
2020-10-29 22:42:22 +00:00
}
for _ , test := range tests {
2022-03-10 20:26:22 +00:00
test := test
t . Run ( test . Name , func ( t * testing . T ) {
scheme := test . Scheme
addr := test . Address
caBundle := test . CABundle
if addr == "" {
// Both cases are not required, so when one is empty skip it.
t . Skip ( "no address defined" )
}
// Test that there is no default discovery endpoint available when there are no FederationDomains.
requireDiscoveryEndpointsAreNotFound ( t , scheme , addr , caBundle , fmt . Sprintf ( "%s://%s" , scheme , addr ) )
// Define several unique issuer strings. Always use https in the issuer name even when we are accessing the http port.
issuer1 := fmt . Sprintf ( "https://%s/nested/issuer1" , addr )
issuer2 := fmt . Sprintf ( "https://%s/nested/issuer2" , addr )
issuer3 := fmt . Sprintf ( "https://%s/issuer3" , addr )
issuer4 := fmt . Sprintf ( "https://%s/issuer4" , addr )
issuer5 := fmt . Sprintf ( "https://%s/issuer5" , addr )
issuer6 := fmt . Sprintf ( "https://%s/issuer6" , addr )
badIssuer := fmt . Sprintf ( "https://%s/badIssuer?cannot-use=queries" , addr )
// When FederationDomain are created in sequence they each cause a discovery endpoint to appear only for as long as the FederationDomain exists.
config1 , jwks1 := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear ( ctx , t , scheme , addr , caBundle , issuer1 , client )
requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear ( t , config1 , client , ns , scheme , addr , caBundle , issuer1 )
config2 , jwks2 := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear ( ctx , t , scheme , addr , caBundle , issuer2 , client )
requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear ( t , config2 , client , ns , scheme , addr , caBundle , issuer2 )
// The auto-created JWK's were different from each other.
require . NotEqual ( t , jwks1 . Keys [ 0 ] [ "x" ] , jwks2 . Keys [ 0 ] [ "x" ] )
require . NotEqual ( t , jwks1 . Keys [ 0 ] [ "y" ] , jwks2 . Keys [ 0 ] [ "y" ] )
// When multiple FederationDomains exist at the same time they each serve a unique discovery endpoint.
config3 , jwks3 := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear ( ctx , t , scheme , addr , caBundle , issuer3 , client )
config4 , jwks4 := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear ( ctx , t , scheme , addr , caBundle , issuer4 , client )
requireDiscoveryEndpointsAreWorking ( t , scheme , addr , caBundle , issuer3 , nil ) // discovery for issuer3 is still working after issuer4 started working
// The auto-created JWK's were different from each other.
require . NotEqual ( t , jwks3 . Keys [ 0 ] [ "x" ] , jwks4 . Keys [ 0 ] [ "x" ] )
require . NotEqual ( t , jwks3 . Keys [ 0 ] [ "y" ] , jwks4 . Keys [ 0 ] [ "y" ] )
// Editing a provider to change the issuer name updates the endpoints that are being served.
updatedConfig4 := editFederationDomainIssuerName ( t , config4 , client , ns , issuer5 )
requireDiscoveryEndpointsAreNotFound ( t , scheme , addr , caBundle , issuer4 )
jwks5 := requireDiscoveryEndpointsAreWorking ( t , scheme , addr , caBundle , issuer5 , nil )
// The JWK did not change when the issuer name was updated.
require . Equal ( t , jwks4 . Keys [ 0 ] , jwks5 . Keys [ 0 ] )
// When they are deleted they stop serving discovery endpoints.
requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear ( t , config3 , client , ns , scheme , addr , caBundle , issuer3 )
requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear ( t , updatedConfig4 , client , ns , scheme , addr , caBundle , issuer5 )
// When the same issuer is added twice, both issuers are marked as duplicates, and neither provider is serving.
config6Duplicate1 , _ := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear ( ctx , t , scheme , addr , caBundle , issuer6 , client )
config6Duplicate2 := testlib . CreateTestFederationDomain ( ctx , t , issuer6 , "" , "" )
requireStatus ( t , client , ns , config6Duplicate1 . Name , v1alpha1 . DuplicateFederationDomainStatusCondition )
requireStatus ( t , client , ns , config6Duplicate2 . Name , v1alpha1 . DuplicateFederationDomainStatusCondition )
requireDiscoveryEndpointsAreNotFound ( t , scheme , addr , caBundle , issuer6 )
// If we delete the first duplicate issuer, the second duplicate issuer starts serving.
requireDelete ( t , client , ns , config6Duplicate1 . Name )
requireWellKnownEndpointIsWorking ( t , scheme , addr , caBundle , issuer6 , nil )
requireStatus ( t , client , ns , config6Duplicate2 . Name , v1alpha1 . SuccessFederationDomainStatusCondition )
// When we finally delete all issuers, the endpoint should be down.
requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear ( t , config6Duplicate2 , client , ns , scheme , addr , caBundle , issuer6 )
2020-10-30 20:19:23 +00:00
// "Host" headers can be used to send requests to discovery endpoints when the public address is different from the issuer name.
issuer7 := "https://some-issuer-host-and-port-that-doesnt-match-public-supervisor-address.com:2684/issuer7"
2020-12-16 22:27:09 +00:00
config7 , _ := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear ( ctx , t , scheme , addr , caBundle , issuer7 , client )
requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear ( t , config7 , client , ns , scheme , addr , caBundle , issuer7 )
2020-10-29 22:42:22 +00:00
2022-03-10 20:26:22 +00:00
// When we create a provider with an invalid issuer, the status is set to invalid.
badConfig := testlib . CreateTestFederationDomain ( ctx , t , badIssuer , "" , "" )
requireStatus ( t , client , ns , badConfig . Name , v1alpha1 . InvalidFederationDomainStatusCondition )
requireDiscoveryEndpointsAreNotFound ( t , scheme , addr , caBundle , badIssuer )
requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear ( t , badConfig , client , ns , scheme , addr , caBundle , badIssuer )
} )
2020-10-29 22:42:22 +00:00
}
}
2021-08-26 18:31:50 +00:00
// Never run this test in parallel since deleting all federation domains is disruptive, see main_test.go.
func TestSupervisorTLSTerminationWithSNI_Disruptive ( t * testing . T ) {
2021-06-22 15:23:19 +00:00
env := testlib . IntegrationEnv ( t )
pinnipedClient := testlib . NewSupervisorClientset ( t )
kubeClient := testlib . NewKubernetesClientset ( t )
2020-10-06 19:20:29 +00:00
2020-10-07 00:53:29 +00:00
ns := env . SupervisorNamespace
2020-12-16 23:13:02 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 10 * time . Minute )
2020-10-06 19:20:29 +00:00
defer cancel ( )
2020-12-16 22:27:09 +00:00
temporarilyRemoveAllFederationDomainsAndDefaultTLSCertSecret ( ctx , t , ns , defaultTLSCertSecretName ( env ) , pinnipedClient , kubeClient )
2020-10-27 21:57:25 +00:00
scheme := "https"
2020-11-02 16:57:05 +00:00
address := env . SupervisorHTTPSAddress // hostname and port for direct access to the supervisor's port 8443
2020-10-27 21:57:25 +00:00
hostname1 := strings . Split ( address , ":" ) [ 0 ]
issuer1 := fmt . Sprintf ( "%s://%s/issuer1" , scheme , address )
2020-11-02 22:55:29 +00:00
certSecretName1 := "integration-test-cert-1"
2020-10-27 21:57:25 +00:00
2020-12-16 22:27:09 +00:00
// Create an FederationDomain with a spec.tls.secretName.
2021-06-22 15:23:19 +00:00
federationDomain1 := testlib . CreateTestFederationDomain ( ctx , t , issuer1 , certSecretName1 , "" )
2020-12-16 22:27:09 +00:00
requireStatus ( t , pinnipedClient , federationDomain1 . Namespace , federationDomain1 . Name , v1alpha1 . SuccessFederationDomainStatusCondition )
2020-10-27 21:57:25 +00:00
2020-11-02 22:55:29 +00:00
// The spec.tls.secretName Secret does not exist, so the endpoints should fail with TLS errors.
2021-12-15 20:48:55 +00:00
requireEndpointHasBootstrapTLSErrorBecauseCertificatesAreNotReady ( t , issuer1 )
2020-10-27 21:57:25 +00:00
// Create the Secret.
2020-11-02 22:55:29 +00:00
ca1 := createTLSCertificateSecret ( ctx , t , ns , hostname1 , nil , certSecretName1 , kubeClient )
2020-10-27 21:57:25 +00:00
// Now that the Secret exists, we should be able to access the endpoints by hostname using the CA.
_ = requireDiscoveryEndpointsAreWorking ( t , scheme , address , string ( ca1 . Bundle ( ) ) , issuer1 , nil )
2020-11-02 22:55:29 +00:00
// Update the config to with a new .spec.tls.secretName.
certSecretName1update := "integration-test-cert-1-update"
2021-03-11 19:18:15 +00:00
require . NoError ( t , retry . RetryOnConflict ( retry . DefaultRetry , func ( ) error {
federationDomain1LatestVersion , err := pinnipedClient . ConfigV1alpha1 ( ) . FederationDomains ( ns ) . Get ( ctx , federationDomain1 . Name , metav1 . GetOptions { } )
if err != nil {
return err
}
federationDomain1LatestVersion . Spec . TLS = & v1alpha1 . FederationDomainTLSSpec { SecretName : certSecretName1update }
_ , err = pinnipedClient . ConfigV1alpha1 ( ) . FederationDomains ( ns ) . Update ( ctx , federationDomain1LatestVersion , metav1 . UpdateOptions { } )
return err
} ) )
2020-10-07 00:53:29 +00:00
2020-10-27 21:57:25 +00:00
// The the endpoints should fail with TLS errors again.
2021-12-15 20:48:55 +00:00
requireEndpointHasBootstrapTLSErrorBecauseCertificatesAreNotReady ( t , issuer1 )
2020-10-07 14:53:05 +00:00
2020-10-27 21:57:25 +00:00
// Create a Secret at the updated name.
2020-11-02 22:55:29 +00:00
ca1update := createTLSCertificateSecret ( ctx , t , ns , hostname1 , nil , certSecretName1update , kubeClient )
2020-10-27 21:57:25 +00:00
// Now that the Secret exists at the new name, we should be able to access the endpoints by hostname using the CA.
_ = requireDiscoveryEndpointsAreWorking ( t , scheme , address , string ( ca1update . Bundle ( ) ) , issuer1 , nil )
// To test SNI virtual hosting, send requests to discovery endpoints when the public address is different from the issuer name.
hostname2 := "some-issuer-host-and-port-that-doesnt-match-public-supervisor-address.com"
hostnamePort2 := "2684"
issuer2 := fmt . Sprintf ( "%s://%s:%s/issuer2" , scheme , hostname2 , hostnamePort2 )
2020-11-02 22:55:29 +00:00
certSecretName2 := "integration-test-cert-2"
2020-10-27 21:57:25 +00:00
2020-12-16 22:27:09 +00:00
// Create an FederationDomain with a spec.tls.secretName.
2021-06-22 15:23:19 +00:00
federationDomain2 := testlib . CreateTestFederationDomain ( ctx , t , issuer2 , certSecretName2 , "" )
2020-12-16 22:27:09 +00:00
requireStatus ( t , pinnipedClient , federationDomain2 . Namespace , federationDomain2 . Name , v1alpha1 . SuccessFederationDomainStatusCondition )
2020-10-27 21:57:25 +00:00
// Create the Secret.
2020-11-02 22:55:29 +00:00
ca2 := createTLSCertificateSecret ( ctx , t , ns , hostname2 , nil , certSecretName2 , kubeClient )
2020-10-27 21:57:25 +00:00
// Now that the Secret exists, we should be able to access the endpoints by hostname using the CA.
_ = requireDiscoveryEndpointsAreWorking ( t , scheme , hostname2 + ":" + hostnamePort2 , string ( ca2 . Bundle ( ) ) , issuer2 , map [ string ] string {
hostname2 + ":" + hostnamePort2 : address ,
2020-10-07 00:53:29 +00:00
} )
2020-10-27 21:57:25 +00:00
}
2021-08-26 18:31:50 +00:00
// Never run this test in parallel since deleting all federation domains is disruptive, see main_test.go.
func TestSupervisorTLSTerminationWithDefaultCerts_Disruptive ( t * testing . T ) {
2021-06-22 15:23:19 +00:00
env := testlib . IntegrationEnv ( t )
pinnipedClient := testlib . NewSupervisorClientset ( t )
kubeClient := testlib . NewKubernetesClientset ( t )
2020-10-28 15:58:50 +00:00
ns := env . SupervisorNamespace
2020-12-16 23:13:02 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 10 * time . Minute )
2020-10-28 15:58:50 +00:00
defer cancel ( )
2020-12-16 22:27:09 +00:00
temporarilyRemoveAllFederationDomainsAndDefaultTLSCertSecret ( ctx , t , ns , defaultTLSCertSecretName ( env ) , pinnipedClient , kubeClient )
2020-10-28 15:58:50 +00:00
scheme := "https"
2020-11-02 16:57:05 +00:00
address := env . SupervisorHTTPSAddress // hostname and port for direct access to the supervisor's port 8443
2020-10-28 15:58:50 +00:00
hostAndPortSegments := strings . Split ( address , ":" )
2020-10-28 20:09:20 +00:00
// hostnames are case-insensitive, so test mis-matching the case of the issuer URL and the request URL
hostname := strings . ToLower ( hostAndPortSegments [ 0 ] )
2020-11-02 16:57:05 +00:00
port := "8443"
2020-10-28 15:58:50 +00:00
if len ( hostAndPortSegments ) > 1 {
port = hostAndPortSegments [ 1 ]
}
2020-11-24 19:38:28 +00:00
2021-06-22 15:23:19 +00:00
ips , err := testlib . LookupIP ( ctx , hostname )
2020-10-28 15:58:50 +00:00
require . NoError ( t , err )
2020-12-02 23:49:21 +00:00
require . NotEmpty ( t , ips )
ipWithPort := ips [ 0 ] . String ( ) + ":" + port
2020-10-28 15:58:50 +00:00
issuerUsingIPAddress := fmt . Sprintf ( "%s://%s/issuer1" , scheme , ipWithPort )
issuerUsingHostname := fmt . Sprintf ( "%s://%s/issuer1" , scheme , address )
2020-12-16 22:27:09 +00:00
// Create an FederationDomain without a spec.tls.secretName.
2021-06-22 15:23:19 +00:00
federationDomain1 := testlib . CreateTestFederationDomain ( ctx , t , issuerUsingIPAddress , "" , "" )
2020-12-16 22:27:09 +00:00
requireStatus ( t , pinnipedClient , federationDomain1 . Namespace , federationDomain1 . Name , v1alpha1 . SuccessFederationDomainStatusCondition )
2020-10-28 15:58:50 +00:00
2020-11-02 22:55:29 +00:00
// There is no default TLS cert and the spec.tls.secretName was not set, so the endpoints should fail with TLS errors.
2021-12-15 20:48:55 +00:00
requireEndpointHasBootstrapTLSErrorBecauseCertificatesAreNotReady ( t , issuerUsingIPAddress )
2020-10-28 15:58:50 +00:00
// Create a Secret at the special name which represents the default TLS cert.
2020-12-02 23:49:21 +00:00
defaultCA := createTLSCertificateSecret ( ctx , t , ns , "cert-hostname-doesnt-matter" , [ ] net . IP { ips [ 0 ] } , defaultTLSCertSecretName ( env ) , kubeClient )
2020-10-28 15:58:50 +00:00
// Now that the Secret exists, we should be able to access the endpoints by IP address using the CA.
_ = requireDiscoveryEndpointsAreWorking ( t , scheme , ipWithPort , string ( defaultCA . Bundle ( ) ) , issuerUsingIPAddress , nil )
2020-12-16 22:27:09 +00:00
// Create an FederationDomain with a spec.tls.secretName.
2020-11-02 22:55:29 +00:00
certSecretName := "integration-test-cert-1"
2021-06-22 15:23:19 +00:00
federationDomain2 := testlib . CreateTestFederationDomain ( ctx , t , issuerUsingHostname , certSecretName , "" )
2020-12-16 22:27:09 +00:00
requireStatus ( t , pinnipedClient , federationDomain2 . Namespace , federationDomain2 . Name , v1alpha1 . SuccessFederationDomainStatusCondition )
2020-10-28 15:58:50 +00:00
// Create the Secret.
2020-11-02 22:55:29 +00:00
certCA := createTLSCertificateSecret ( ctx , t , ns , hostname , nil , certSecretName , kubeClient )
2020-10-28 15:58:50 +00:00
// Now that the Secret exists, we should be able to access the endpoints by hostname using the CA from the SNI cert.
2020-10-28 20:09:20 +00:00
// Hostnames are case-insensitive, so the request should still work even if the case of the hostname is different
// from the case of the issuer URL's hostname.
2020-11-02 22:55:29 +00:00
_ = requireDiscoveryEndpointsAreWorking ( t , scheme , strings . ToUpper ( hostname ) + ":" + port , string ( certCA . Bundle ( ) ) , issuerUsingHostname , nil )
2020-10-28 15:58:50 +00:00
// And we can still access the other issuer using the default cert.
_ = requireDiscoveryEndpointsAreWorking ( t , scheme , ipWithPort , string ( defaultCA . Bundle ( ) ) , issuerUsingIPAddress , nil )
}
2021-06-22 15:23:19 +00:00
func defaultTLSCertSecretName ( env * testlib . TestEnv ) string {
2022-03-08 20:28:09 +00:00
return env . SupervisorAppName + "-default-tls-certificate"
2020-10-28 23:11:19 +00:00
}
2020-10-27 23:33:08 +00:00
func createTLSCertificateSecret ( ctx context . Context , t * testing . T , ns string , hostname string , ips [ ] net . IP , secretName string , kubeClient kubernetes . Interface ) * certauthority . CA {
2020-10-27 21:57:25 +00:00
// Create a CA.
2021-03-13 00:09:16 +00:00
ca , err := certauthority . New ( "Acme Corp" , 1000 * time . Hour )
2020-10-27 21:57:25 +00:00
require . NoError ( t , err )
// Using the CA, create a TLS server cert.
2021-03-13 00:09:16 +00:00
tlsCert , err := ca . IssueServerCert ( [ ] string { hostname } , ips , 1000 * time . Hour )
2020-10-27 21:57:25 +00:00
require . NoError ( t , err )
// Write the serving cert to the SNI secret.
tlsCertChainPEM , tlsPrivateKeyPEM , err := certauthority . ToPEM ( tlsCert )
require . NoError ( t , err )
secret := corev1 . Secret {
2020-12-18 23:10:17 +00:00
Type : corev1 . SecretTypeTLS ,
2020-10-27 21:57:25 +00:00
TypeMeta : metav1 . TypeMeta { } ,
ObjectMeta : metav1 . ObjectMeta {
2020-10-27 23:33:08 +00:00
Name : secretName ,
2020-10-27 21:57:25 +00:00
Namespace : ns ,
} ,
StringData : map [ string ] string {
"tls.crt" : string ( tlsCertChainPEM ) ,
"tls.key" : string ( tlsPrivateKeyPEM ) ,
} ,
}
_ , err = kubeClient . CoreV1 ( ) . Secrets ( ns ) . Create ( ctx , & secret , metav1 . CreateOptions { } )
require . NoError ( t , err )
// Delete the Secret when the test ends.
t . Cleanup ( func ( ) {
t . Helper ( )
2020-12-16 23:13:02 +00:00
deleteCtx , cancel := context . WithTimeout ( context . Background ( ) , 2 * time . Minute )
2020-10-27 21:57:25 +00:00
defer cancel ( )
2020-10-27 23:33:08 +00:00
err := kubeClient . CoreV1 ( ) . Secrets ( ns ) . Delete ( deleteCtx , secretName , metav1 . DeleteOptions { } )
2020-10-27 21:57:25 +00:00
require . NoError ( t , err )
} )
return ca
}
2020-12-16 22:27:09 +00:00
func temporarilyRemoveAllFederationDomainsAndDefaultTLSCertSecret (
2020-10-28 23:11:19 +00:00
ctx context . Context ,
t * testing . T ,
ns string ,
defaultTLSCertSecretName string ,
pinnipedClient pinnipedclientset . Interface ,
kubeClient kubernetes . Interface ,
) {
2020-12-16 22:27:09 +00:00
// Temporarily remove any existing FederationDomains from the cluster so we can test from a clean slate.
originalConfigList , err := pinnipedClient . ConfigV1alpha1 ( ) . FederationDomains ( ns ) . List ( ctx , metav1 . ListOptions { } )
2020-10-27 21:57:25 +00:00
require . NoError ( t , err )
for _ , config := range originalConfigList . Items {
2020-12-16 22:27:09 +00:00
err := pinnipedClient . ConfigV1alpha1 ( ) . FederationDomains ( ns ) . Delete ( ctx , config . Name , metav1 . DeleteOptions { } )
2020-10-27 21:57:25 +00:00
require . NoError ( t , err )
}
2020-10-28 19:32:21 +00:00
// Also remove the supervisor's default TLS cert
2020-10-28 23:11:19 +00:00
originalSecret , err := kubeClient . CoreV1 ( ) . Secrets ( ns ) . Get ( ctx , defaultTLSCertSecretName , metav1 . GetOptions { } )
2020-10-28 19:32:21 +00:00
notFound := k8serrors . IsNotFound ( err )
2020-10-28 23:11:19 +00:00
require . False ( t , err != nil && ! notFound , "unexpected error when getting %s" , defaultTLSCertSecretName )
2020-10-28 21:25:01 +00:00
if notFound {
originalSecret = nil
} else {
2020-10-28 23:11:19 +00:00
err = kubeClient . CoreV1 ( ) . Secrets ( ns ) . Delete ( ctx , defaultTLSCertSecretName , metav1 . DeleteOptions { } )
2020-10-28 21:25:01 +00:00
require . NoError ( t , err )
}
2020-10-28 19:32:21 +00:00
2020-12-16 22:27:09 +00:00
// When this test has finished, recreate any FederationDomains and default secret that had existed on the cluster before this test.
2020-10-27 21:57:25 +00:00
t . Cleanup ( func ( ) {
cleanupCtx , cancel := context . WithTimeout ( context . Background ( ) , 2 * time . Minute )
defer cancel ( )
for _ , config := range originalConfigList . Items {
thisConfig := config
thisConfig . ResourceVersion = "" // Get rid of resource version since we can't create an object with one.
2020-12-16 22:27:09 +00:00
_ , err := pinnipedClient . ConfigV1alpha1 ( ) . FederationDomains ( ns ) . Create ( cleanupCtx , & thisConfig , metav1 . CreateOptions { } )
2020-10-28 19:32:21 +00:00
require . NoError ( t , err )
}
if originalSecret != nil {
originalSecret . ResourceVersion = "" // Get rid of resource version since we can't create an object with one.
_ , err = kubeClient . CoreV1 ( ) . Secrets ( ns ) . Create ( cleanupCtx , originalSecret , metav1 . CreateOptions { } )
2020-10-27 21:57:25 +00:00
require . NoError ( t , err )
}
} )
}
2020-10-20 21:00:36 +00:00
func jwksURLForIssuer ( scheme , host , path string ) string {
2020-10-28 22:22:53 +00:00
if path == "" {
return fmt . Sprintf ( "%s://%s/jwks.json" , scheme , host )
}
2020-10-20 22:22:03 +00:00
return fmt . Sprintf ( "%s://%s/%s/jwks.json" , scheme , host , strings . TrimPrefix ( path , "/" ) )
2020-10-19 19:21:18 +00:00
}
2020-10-20 21:00:36 +00:00
func wellKnownURLForIssuer ( scheme , host , path string ) string {
2020-10-28 22:22:53 +00:00
if path == "" {
return fmt . Sprintf ( "%s://%s/.well-known/openid-configuration" , scheme , host )
}
2020-10-20 22:22:03 +00:00
return fmt . Sprintf ( "%s://%s/%s/.well-known/openid-configuration" , scheme , host , strings . TrimPrefix ( path , "/" ) )
2020-10-19 19:21:18 +00:00
}
2020-10-20 23:46:33 +00:00
func requireDiscoveryEndpointsAreNotFound ( t * testing . T , supervisorScheme , supervisorAddress , supervisorCABundle , issuerName string ) {
2020-10-19 19:21:18 +00:00
t . Helper ( )
2020-10-20 21:00:36 +00:00
issuerURL , err := url . Parse ( issuerName )
require . NoError ( t , err )
2020-10-20 23:46:33 +00:00
requireEndpointNotFound ( t , wellKnownURLForIssuer ( supervisorScheme , supervisorAddress , issuerURL . Path ) , issuerURL . Host , supervisorCABundle )
requireEndpointNotFound ( t , jwksURLForIssuer ( supervisorScheme , supervisorAddress , issuerURL . Path ) , issuerURL . Host , supervisorCABundle )
2020-10-19 19:21:18 +00:00
}
2020-10-20 23:46:33 +00:00
func requireEndpointNotFound ( t * testing . T , url , host , caBundle string ) {
2020-10-08 02:18:34 +00:00
t . Helper ( )
2020-10-27 21:57:25 +00:00
httpClient := newHTTPClient ( t , caBundle , nil )
2020-10-08 02:18:34 +00:00
2021-08-27 20:10:06 +00:00
testlib . RequireEventually ( t , func ( requireEventually * require . Assertions ) {
ctx , cancel := context . WithTimeout ( context . Background ( ) , 10 * time . Second )
defer cancel ( )
requestNonExistentPath , err := http . NewRequestWithContext ( ctx , http . MethodGet , url , nil )
require . NoError ( t , err )
2020-10-20 21:00:36 +00:00
2021-08-27 20:10:06 +00:00
requestNonExistentPath . Host = host
2020-10-08 02:18:34 +00:00
2021-06-16 22:51:23 +00:00
response , err := httpClient . Do ( requestNonExistentPath )
requireEventually . NoError ( err )
requireEventually . NoError ( response . Body . Close ( ) )
requireEventually . Equal ( http . StatusNotFound , response . StatusCode )
2021-08-27 18:56:51 +00:00
} , 2 * time . Minute , 200 * time . Millisecond )
2020-10-08 02:18:34 +00:00
}
2020-10-07 00:53:29 +00:00
2021-12-15 20:48:55 +00:00
func requireEndpointHasBootstrapTLSErrorBecauseCertificatesAreNotReady ( t * testing . T , url string ) {
2020-10-27 21:57:25 +00:00
t . Helper ( )
2021-12-15 20:48:55 +00:00
httpClient := & http . Client {
Transport : & http . Transport {
TLSClientConfig : & tls . Config { InsecureSkipVerify : true } , //nolint:gosec // there is no way for us to know the bootstrap CA
} ,
}
2020-10-27 21:57:25 +00:00
2021-06-22 15:23:19 +00:00
testlib . RequireEventually ( t , func ( requireEventually * require . Assertions ) {
2021-08-27 20:10:06 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 10 * time . Second )
defer cancel ( )
2021-06-16 22:51:23 +00:00
request , err := http . NewRequestWithContext ( ctx , http . MethodGet , url , nil )
requireEventually . NoError ( err )
2020-10-27 21:57:25 +00:00
2021-06-16 22:51:23 +00:00
response , err := httpClient . Do ( request )
2021-12-15 20:48:55 +00:00
requireEventually . NoError ( err )
t . Cleanup ( func ( ) {
2021-06-16 22:51:23 +00:00
_ = response . Body . Close ( )
2021-12-15 20:48:55 +00:00
} )
requireEventually . Equal ( http . StatusInternalServerError , response . StatusCode )
body , err := io . ReadAll ( response . Body )
requireEventually . NoError ( err )
requireEventually . Equal ( "pinniped supervisor has invalid TLS serving certificate configuration\n" , string ( body ) )
2021-08-27 18:56:51 +00:00
} , 2 * time . Minute , 200 * time . Millisecond )
2020-10-27 21:57:25 +00:00
}
2020-12-16 22:27:09 +00:00
func requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear (
2020-10-15 13:09:49 +00:00
ctx context . Context ,
t * testing . T ,
2020-10-20 23:46:33 +00:00
supervisorScheme , supervisorAddress , supervisorCABundle string ,
2020-10-15 13:09:49 +00:00
issuerName string ,
client pinnipedclientset . Interface ,
2020-12-16 22:27:09 +00:00
) ( * v1alpha1 . FederationDomain , * ExpectedJWKSResponseFormat ) {
2020-10-08 02:18:34 +00:00
t . Helper ( )
2021-06-22 15:23:19 +00:00
newFederationDomain := testlib . CreateTestFederationDomain ( ctx , t , issuerName , "" , "" )
2020-10-27 21:57:25 +00:00
jwksResult := requireDiscoveryEndpointsAreWorking ( t , supervisorScheme , supervisorAddress , supervisorCABundle , issuerName , nil )
2020-12-16 22:27:09 +00:00
requireStatus ( t , client , newFederationDomain . Namespace , newFederationDomain . Name , v1alpha1 . SuccessFederationDomainStatusCondition )
return newFederationDomain , jwksResult
2020-10-08 02:18:34 +00:00
}
2020-10-27 21:57:25 +00:00
func requireDiscoveryEndpointsAreWorking ( t * testing . T , supervisorScheme , supervisorAddress , supervisorCABundle , issuerName string , dnsOverrides map [ string ] string ) * ExpectedJWKSResponseFormat {
requireWellKnownEndpointIsWorking ( t , supervisorScheme , supervisorAddress , supervisorCABundle , issuerName , dnsOverrides )
jwksResult := requireJWKSEndpointIsWorking ( t , supervisorScheme , supervisorAddress , supervisorCABundle , issuerName , dnsOverrides )
2020-10-19 19:21:18 +00:00
return jwksResult
}
2020-12-16 22:27:09 +00:00
func requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear (
2020-10-19 19:21:18 +00:00
t * testing . T ,
2020-12-16 22:27:09 +00:00
existingFederationDomain * v1alpha1 . FederationDomain ,
2020-10-19 19:21:18 +00:00
client pinnipedclientset . Interface ,
ns string ,
2020-10-20 23:46:33 +00:00
supervisorScheme , supervisorAddress , supervisorCABundle string ,
2020-10-19 19:21:18 +00:00
issuerName string ,
) {
2020-10-08 02:18:34 +00:00
t . Helper ( )
ctx , cancel := context . WithTimeout ( context . Background ( ) , 2 * time . Minute )
defer cancel ( )
2020-12-16 22:27:09 +00:00
// Delete the FederationDomain.
err := client . ConfigV1alpha1 ( ) . FederationDomains ( ns ) . Delete ( ctx , existingFederationDomain . 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.
2020-10-20 23:46:33 +00:00
requireDiscoveryEndpointsAreNotFound ( t , supervisorScheme , supervisorAddress , supervisorCABundle , issuerName )
2020-10-08 02:18:34 +00:00
}
2020-10-07 14:53:05 +00:00
2020-10-27 21:57:25 +00:00
func requireWellKnownEndpointIsWorking ( t * testing . T , supervisorScheme , supervisorAddress , supervisorCABundle , issuerName string , dnsOverrides map [ string ] string ) {
2020-10-08 02:18:34 +00:00
t . Helper ( )
2020-10-20 21:00:36 +00:00
issuerURL , err := url . Parse ( issuerName )
require . NoError ( t , err )
2020-10-27 21:57:25 +00:00
response , responseBody := requireSuccessEndpointResponse ( t , wellKnownURLForIssuer ( supervisorScheme , supervisorAddress , issuerURL . Path ) , issuerName , supervisorCABundle , dnsOverrides ) //nolint:bodyclose
2020-10-19 19:21:18 +00:00
// Check that the response matches our expectations.
expectedResultTemplate := here . Doc ( ` {
"issuer" : "%s" ,
"authorization_endpoint" : "%s/oauth2/authorize" ,
"token_endpoint" : "%s/oauth2/token" ,
"token_endpoint_auth_methods_supported" : [ "client_secret_basic" ] ,
"jwks_uri" : "%s/jwks.json" ,
"scopes_supported" : [ "openid" , "offline" ] ,
"response_types_supported" : [ "code" ] ,
2021-06-16 19:05:29 +00:00
"response_modes_supported" : [ "query" , "form_post" ] ,
2022-04-17 23:06:59 +00:00
"code_challenge_methods_supported" : [ "S256" ] ,
2020-10-19 19:21:18 +00:00
"claims_supported" : [ "groups" ] ,
2021-05-13 20:07:31 +00:00
"discovery.supervisor.pinniped.dev/v1alpha1" : { "pinniped_identity_providers_endpoint" : "%s/v1alpha1/pinniped_identity_providers" } ,
2020-10-19 19:21:18 +00:00
"subject_types_supported" : [ "public" ] ,
2021-05-13 20:07:31 +00:00
"id_token_signing_alg_values_supported" : [ "ES256" ]
2020-10-19 19:21:18 +00:00
} ` )
2021-05-13 20:07:31 +00:00
expectedJSON := fmt . Sprintf ( expectedResultTemplate , issuerName , issuerName , issuerName , issuerName , issuerName )
2020-10-19 19:21:18 +00:00
require . Equal ( t , "application/json" , response . Header . Get ( "content-type" ) )
require . JSONEq ( t , expectedJSON , responseBody )
}
type ExpectedJWKSResponseFormat struct {
Keys [ ] map [ string ] string
}
2020-10-27 21:57:25 +00:00
func requireJWKSEndpointIsWorking ( t * testing . T , supervisorScheme , supervisorAddress , supervisorCABundle , issuerName string , dnsOverrides map [ string ] string ) * ExpectedJWKSResponseFormat {
2020-10-19 19:21:18 +00:00
t . Helper ( )
2020-10-20 21:00:36 +00:00
issuerURL , err := url . Parse ( issuerName )
require . NoError ( t , err )
2020-10-27 21:57:25 +00:00
response , responseBody := requireSuccessEndpointResponse ( t , //nolint:bodyclose
jwksURLForIssuer ( supervisorScheme , supervisorAddress , issuerURL . Path ) ,
issuerName ,
supervisorCABundle ,
dnsOverrides ,
)
2020-10-19 19:21:18 +00:00
var result ExpectedJWKSResponseFormat
2020-10-20 21:00:36 +00:00
err = json . Unmarshal ( [ ] byte ( responseBody ) , & result )
2020-10-19 19:21:18 +00:00
require . NoError ( t , err )
require . Len ( t , result . Keys , 1 )
jwk := result . Keys [ 0 ]
require . Len ( t , jwk , 7 ) // make sure there are no extra values, i.e. does not include private key
require . NotEmpty ( t , jwk [ "kid" ] )
require . Equal ( t , "sig" , jwk [ "use" ] )
require . Equal ( t , "EC" , jwk [ "kty" ] )
require . Equal ( t , "P-256" , jwk [ "crv" ] )
require . Equal ( t , "ES256" , jwk [ "alg" ] )
require . NotEmpty ( t , jwk [ "x" ] )
require . NotEmpty ( t , jwk [ "y" ] )
require . Equal ( t , "application/json" , response . Header . Get ( "content-type" ) )
return & result
}
2020-10-27 21:57:25 +00:00
func requireSuccessEndpointResponse ( t * testing . T , endpointURL , issuer , caBundle string , dnsOverrides map [ string ] string ) ( * http . Response , string ) {
2020-10-20 23:46:33 +00:00
t . Helper ( )
2020-10-27 21:57:25 +00:00
httpClient := newHTTPClient ( t , caBundle , dnsOverrides )
2020-10-20 22:22:03 +00:00
issuerURL , err := url . Parse ( issuer )
require . NoError ( t , err )
2020-10-20 21:00:36 +00:00
2020-10-07 00:53:29 +00:00
// Fetch that discovery endpoint. Give it some time for the endpoint to come into existence.
var response * http . Response
2021-06-16 22:51:23 +00:00
var responseBody [ ] byte
2021-06-22 15:23:19 +00:00
testlib . RequireEventually ( t , func ( requireEventually * require . Assertions ) {
2021-08-27 20:10:06 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 10 * time . Second )
defer cancel ( )
// Define a request to the new discovery endpoint which should have been created by an FederationDomain.
requestDiscoveryEndpoint , err := http . NewRequestWithContext (
ctx ,
http . MethodGet ,
endpointURL ,
nil ,
)
requireEventually . NoError ( err )
// Set the host header on the request to match the issuer's hostname, which could potentially be different
// from the public ingress address, e.g. when a load balancer is used, so we want to test here that the host
// header is respected by the supervisor server.
requestDiscoveryEndpoint . Host = issuerURL . Host
2021-06-16 22:51:23 +00:00
response , err = httpClient . Do ( requestDiscoveryEndpoint )
requireEventually . NoError ( err )
defer func ( ) { _ = response . Body . Close ( ) } ( )
requireEventually . Equal ( http . StatusOK , response . StatusCode )
2022-08-24 21:45:55 +00:00
responseBody , err = io . ReadAll ( response . Body )
2021-06-16 22:51:23 +00:00
requireEventually . NoError ( err )
2021-08-27 18:56:51 +00:00
} , 2 * time . Minute , 200 * time . Millisecond )
2020-10-07 14:53:05 +00:00
2020-10-19 19:21:18 +00:00
return response , string ( responseBody )
}
2020-10-07 00:53:29 +00:00
2020-12-16 22:27:09 +00:00
func editFederationDomainIssuerName (
2020-10-19 19:21:18 +00:00
t * testing . T ,
2020-12-16 22:27:09 +00:00
existingFederationDomain * v1alpha1 . FederationDomain ,
2020-10-19 19:21:18 +00:00
client pinnipedclientset . Interface ,
ns string ,
newIssuerName string ,
2020-12-16 22:27:09 +00:00
) * v1alpha1 . FederationDomain {
2020-10-19 19:21:18 +00:00
t . Helper ( )
ctx , cancel := context . WithTimeout ( context . Background ( ) , 2 * time . Minute )
defer cancel ( )
2020-10-06 19:20:29 +00:00
2021-03-11 19:18:15 +00:00
var updated * v1alpha1 . FederationDomain
require . NoError ( t , retry . RetryOnConflict ( retry . DefaultRetry , func ( ) error {
mostRecentVersion , err := client . ConfigV1alpha1 ( ) . FederationDomains ( ns ) . Get ( ctx , existingFederationDomain . Name , metav1 . GetOptions { } )
if err != nil {
return err
}
mostRecentVersion . Spec . Issuer = newIssuerName
updated , err = client . ConfigV1alpha1 ( ) . FederationDomains ( ns ) . Update ( ctx , mostRecentVersion , metav1 . UpdateOptions { } )
return err
} ) )
2020-10-19 19:21:18 +00:00
return updated
2020-10-06 19:20:29 +00:00
}
2020-10-08 02:18:34 +00:00
2020-10-08 17:27:45 +00:00
func requireDelete ( t * testing . T , client pinnipedclientset . Interface , ns , name string ) {
t . Helper ( )
2020-12-16 23:13:02 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 2 * time . Minute )
2020-10-08 17:27:45 +00:00
defer cancel ( )
2020-12-16 22:27:09 +00:00
err := client . ConfigV1alpha1 ( ) . FederationDomains ( ns ) . Delete ( ctx , name , metav1 . DeleteOptions { } )
2020-10-08 17:27:45 +00:00
require . NoError ( t , err )
}
2020-12-16 22:27:09 +00:00
func requireStatus ( t * testing . T , client pinnipedclientset . Interface , ns , name string , status v1alpha1 . FederationDomainStatusCondition ) {
2020-10-08 17:27:45 +00:00
t . Helper ( )
2021-06-22 15:23:19 +00:00
testlib . RequireEventually ( t , func ( requireEventually * require . Assertions ) {
2021-03-22 16:33:02 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 30 * time . Second )
defer cancel ( )
2021-06-16 22:51:23 +00:00
federationDomain , err := client . ConfigV1alpha1 ( ) . FederationDomains ( ns ) . Get ( ctx , name , metav1 . GetOptions { } )
requireEventually . NoError ( err )
2021-03-22 16:33:02 +00:00
2021-06-16 22:51:23 +00:00
t . Logf ( "found FederationDomain %s/%s with status %s" , ns , name , federationDomain . Status . Status )
requireEventually . Equalf ( status , federationDomain . Status . Status , "unexpected status (message = '%s')" , federationDomain . Status . Message )
2021-03-22 16:33:02 +00:00
} , 5 * time . Minute , 200 * time . Millisecond )
2020-10-08 17:27:45 +00:00
}
2020-10-20 23:46:33 +00:00
2020-10-27 21:57:25 +00:00
func newHTTPClient ( t * testing . T , caBundle string , dnsOverrides map [ string ] string ) * http . Client {
2020-10-20 23:46:33 +00:00
c := & http . Client { }
2020-10-27 21:57:25 +00:00
realDialer := & net . Dialer { }
overrideDialContext := func ( ctx context . Context , network , addr string ) ( net . Conn , error ) {
replacementAddr , hasKey := dnsOverrides [ addr ]
if hasKey {
t . Logf ( "DialContext replacing addr %s with %s" , addr , replacementAddr )
addr = replacementAddr
} else if dnsOverrides != nil {
t . Fatal ( "dnsOverrides was provided but not used, which was probably a mistake" )
}
return realDialer . DialContext ( ctx , network , addr )
}
2020-10-20 23:46:33 +00:00
if caBundle != "" { // CA bundle is optional
caCertPool := x509 . NewCertPool ( )
caCertPool . AppendCertsFromPEM ( [ ] byte ( caBundle ) )
2020-10-27 21:57:25 +00:00
c . Transport = & http . Transport {
DialContext : overrideDialContext ,
2022-08-24 21:45:55 +00:00
TLSClientConfig : & tls . Config { MinVersion : ptls . SecureTLSConfigMinTLSVersion , RootCAs : caCertPool } , //nolint:gosec // this seems to be a false flag, min tls version is 1.3 in normal mode or 1.2 in fips mode
2020-10-27 21:57:25 +00:00
}
} else {
c . Transport = & http . Transport {
DialContext : overrideDialContext ,
}
2020-10-20 23:46:33 +00:00
}
2020-10-27 21:57:25 +00:00
2020-10-20 23:46:33 +00:00
return c
}