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:
parent
10c4cb4493
commit
1c66ffd5ff
@ -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
@ -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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user