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/tls"
"crypto/x509" "crypto/x509"
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -62,6 +64,8 @@ type getKubeconfigOIDCParams struct {
debugSessionCache bool debugSessionCache bool
caBundle caBundleFlag caBundle caBundleFlag
requestAudience string requestAudience string
upstreamIDPName string
upstreamIDPType string
} }
type getKubeconfigConciergeParams struct { type getKubeconfigConciergeParams struct {
@ -91,6 +95,15 @@ type getKubeconfigParams struct {
credentialCachePathSet bool 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 { func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
var ( var (
cmd = &cobra.Command{ 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.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.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.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.kubeconfigPath, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to kubeconfig file")
f.StringVar(&flags.kubeconfigContextOverride, "kubeconfig-context", "", "Kubeconfig context name (default: current active context)") 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)") 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 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 --credential-cache is set, pass it through.
if flags.credentialCachePathSet { if flags.credentialCachePathSet {
execConfig.Args = append(execConfig.Args, "--credential-cache="+flags.credentialCachePath) 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 != "" { if flags.oidc.requestAudience != "" {
execConfig.Args = append(execConfig.Args, "--request-audience="+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) kubeconfig := newExecKubeconfig(cluster, &execConfig, newKubeconfigNames)
if err := validateKubeconfig(ctx, flags, kubeconfig, deps.log); err != nil { if err := validateKubeconfig(ctx, flags, kubeconfig, deps.log); err != nil {
return err return err
@ -688,3 +716,54 @@ func hasPendingStrategy(credentialIssuer *configv1alpha1.CredentialIssuer) bool
} }
return false 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 // SPDX-License-Identifier: Apache-2.0
package testutil package testutil
import "io" import (
"io"
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/require"
)
// ErrorWriter implements io.Writer by returning a fixed error. // ErrorWriter implements io.Writer by returning a fixed error.
type ErrorWriter struct { type ErrorWriter struct {
@ -13,3 +20,19 @@ type ErrorWriter struct {
var _ io.Writer = &ErrorWriter{} var _ io.Writer = &ErrorWriter{}
func (e *ErrorWriter) Write([]byte) (int, error) { return 0, e.ReturnError } 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
}