WIP: add supervisor upstream flags to pinniped get kubeconfig

- And perform auto-discovery when the flags are not set
- Several TODOs remain which will be addressed in the next commit

Signed-off-by: Margo Crawford <margaretc@vmware.com>
This commit is contained in:
Ryan Richard 2021-04-30 14:28:03 -07:00 committed by Margo Crawford
parent 10c4cb4493
commit 1c66ffd5ff
3 changed files with 1606 additions and 770 deletions

View File

@ -8,8 +8,10 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
@ -62,6 +64,8 @@ type getKubeconfigOIDCParams struct {
debugSessionCache bool
caBundle caBundleFlag
requestAudience string
upstreamIDPName string
upstreamIDPType string
}
type getKubeconfigConciergeParams struct {
@ -91,6 +95,15 @@ type getKubeconfigParams struct {
credentialCachePathSet bool
}
type supervisorDiscoveryResponse struct {
PinnipedIDPs []pinnipedIDPResponse `json:"pinniped_idps"`
}
type pinnipedIDPResponse struct {
Name string `json:"name"`
Type string `json:"type"`
}
func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
var (
cmd = &cobra.Command{
@ -128,6 +141,8 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
f.Var(&flags.oidc.caBundle, "oidc-ca-bundle", "Path to TLS certificate authority bundle (PEM format, optional, can be repeated)")
f.BoolVar(&flags.oidc.debugSessionCache, "oidc-debug-session-cache", false, "Print debug logs related to the OpenID Connect session cache")
f.StringVar(&flags.oidc.requestAudience, "oidc-request-audience", "", "Request a token with an alternate audience using RFC8693 token exchange")
f.StringVar(&flags.oidc.upstreamIDPName, "upstream-identity-provider-name", "", "The name of the upstream identity provider used during login with a Supervisor")
f.StringVar(&flags.oidc.upstreamIDPType, "upstream-identity-provider-type", "", "The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap')")
f.StringVar(&flags.kubeconfigPath, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to kubeconfig file")
f.StringVar(&flags.kubeconfigContextOverride, "kubeconfig-context", "", "Kubeconfig context name (default: current active context)")
f.BoolVar(&flags.skipValidate, "skip-validation", false, "Skip final validation of the kubeconfig (default: false)")
@ -236,6 +251,13 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f
cluster.CertificateAuthorityData = flags.concierge.caBundle
}
// If there is an issuer, and if both upstream flags are not already set, then try to discover Supervisor upstream IDP.
if len(flags.oidc.issuer) > 0 && (flags.oidc.upstreamIDPType == "" || flags.oidc.upstreamIDPName == "") {
if err := discoverSupervisorUpstreamIDP(ctx, &flags); err != nil {
return err
}
}
// If --credential-cache is set, pass it through.
if flags.credentialCachePathSet {
execConfig.Args = append(execConfig.Args, "--credential-cache="+flags.credentialCachePath)
@ -289,6 +311,12 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f
if flags.oidc.requestAudience != "" {
execConfig.Args = append(execConfig.Args, "--request-audience="+flags.oidc.requestAudience)
}
if flags.oidc.upstreamIDPName != "" {
execConfig.Args = append(execConfig.Args, "--upstream-identity-provider-name="+flags.oidc.upstreamIDPName)
}
if flags.oidc.upstreamIDPType != "" {
execConfig.Args = append(execConfig.Args, "--upstream-identity-provider-type="+flags.oidc.upstreamIDPType)
}
kubeconfig := newExecKubeconfig(cluster, &execConfig, newKubeconfigNames)
if err := validateKubeconfig(ctx, flags, kubeconfig, deps.log); err != nil {
return err
@ -688,3 +716,54 @@ func hasPendingStrategy(credentialIssuer *configv1alpha1.CredentialIssuer) bool
}
return false
}
func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigParams) error {
issuerDiscoveryURL := flags.oidc.issuer + "/.well-known/openid-configuration"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, issuerDiscoveryURL, nil)
if err != nil {
return fmt.Errorf("while forming request to issuer URL: %w", err)
}
transport := &http.Transport{TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12}}
httpClient := http.Client{Transport: transport}
if flags.oidc.caBundle != nil {
rootCAs := x509.NewCertPool()
ok := rootCAs.AppendCertsFromPEM(flags.oidc.caBundle)
if !ok {
return fmt.Errorf("unable to fetch discovery data from issuer: could not parse CA bundle")
}
transport.TLSClientConfig.RootCAs = rootCAs
}
response, err := httpClient.Do(request)
if err != nil {
return fmt.Errorf("unable to fetch discovery data from issuer: %w", err)
}
defer func() {
_ = response.Body.Close()
}()
if response.StatusCode == http.StatusNotFound {
// 404 Not Found is not an error because OIDC discovery is an optional part of the OIDC spec.
return nil
}
if response.StatusCode != http.StatusOK {
// Other types of error responses aside from 404 are not expected.
return fmt.Errorf("unable to fetch discovery data from issuer: unexpected http response status: %s", response.Status)
}
rawBody, err := ioutil.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("unable to fetch discovery data from issuer: could not read response body: %w", err)
}
var body supervisorDiscoveryResponse
err = json.Unmarshal(rawBody, &body)
if err != nil {
return fmt.Errorf("unable to fetch discovery data from issuer: could not parse response JSON: %w", err)
}
if len(body.PinnipedIDPs) > 0 {
flags.oidc.upstreamIDPName = body.PinnipedIDPs[0].Name
flags.oidc.upstreamIDPType = body.PinnipedIDPs[0].Type
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,16 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package testutil
import "io"
import (
"io"
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/require"
)
// ErrorWriter implements io.Writer by returning a fixed error.
type ErrorWriter struct {
@ -13,3 +20,19 @@ type ErrorWriter struct {
var _ io.Writer = &ErrorWriter{}
func (e *ErrorWriter) Write([]byte) (int, error) { return 0, e.ReturnError }
func WriteStringToTempFile(t *testing.T, filename string, fileBody string) *os.File {
t.Helper()
f, err := ioutil.TempFile("", filename)
require.NoError(t, err)
deferMe := func() {
err := os.Remove(f.Name())
require.NoError(t, err)
}
t.Cleanup(deferMe)
_, err = f.WriteString(fileBody)
require.NoError(t, err)
err = f.Close()
require.NoError(t, err)
return f
}