diff --git a/cmd/pinniped/cmd/flag_types.go b/cmd/pinniped/cmd/flag_types.go index 262bd9b8..8ca97681 100644 --- a/cmd/pinniped/cmd/flag_types.go +++ b/cmd/pinniped/cmd/flag_types.go @@ -16,6 +16,7 @@ type conciergeMode int var _ flag.Value = new(conciergeMode) const ( + modeUnknown conciergeMode = iota modeTokenCredentialRequestAPI conciergeMode = iota modeImpersonationProxy conciergeMode = iota ) @@ -32,6 +33,10 @@ func (c *conciergeMode) String() string { } func (c *conciergeMode) Set(s string) error { + if strings.EqualFold(s, "") { + *c = modeUnknown + return nil + } if strings.EqualFold(s, "TokenCredentialRequestAPI") { *c = modeTokenCredentialRequestAPI return nil diff --git a/cmd/pinniped/cmd/flag_types_test.go b/cmd/pinniped/cmd/flag_types_test.go index 955ae624..26f107b7 100644 --- a/cmd/pinniped/cmd/flag_types_test.go +++ b/cmd/pinniped/cmd/flag_types_test.go @@ -12,7 +12,7 @@ import ( func TestConciergeModeFlag(t *testing.T) { var m conciergeMode require.Equal(t, "mode", m.Type()) - require.Equal(t, modeTokenCredentialRequestAPI, m) + require.Equal(t, modeUnknown, m) require.EqualError(t, m.Set("foo"), `invalid mode "foo", valid modes are TokenCredentialRequestAPI and ImpersonationProxy`) require.NoError(t, m.Set("TokenCredentialRequestAPI")) diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index 1d1e271d..7bdcff64 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -25,6 +25,7 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth" // Adds handlers for various dynamic auth plugins in client-go conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" + configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" "go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/internal/kubeclient" @@ -74,6 +75,7 @@ type getKubeconfigOIDCParams struct { type getKubeconfigConciergeParams struct { disabled bool + credentialIssuer string authenticatorName string authenticatorType string apiGroupSuffix string @@ -109,6 +111,7 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command { f.BoolVar(&flags.concierge.disabled, "no-concierge", false, "Generate a configuration which does not use the Concierge, but sends the credential to the cluster directly") f.StringVar(&namespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the Concierge was installed") + f.StringVar(&flags.concierge.credentialIssuer, "concierge-credential-issuer", "", "Concierge CredentialIssuer object to use for autodiscovery (default: autodiscover)") f.StringVar(&flags.concierge.authenticatorType, "concierge-authenticator-type", "", "Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover)") f.StringVar(&flags.concierge.authenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name (default: autodiscover)") f.StringVar(&flags.concierge.apiGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix") @@ -178,6 +181,11 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar } if !flags.concierge.disabled { + credentialIssuer, err := lookupCredentialIssuer(clientset, flags.concierge.credentialIssuer) + if err != nil { + return err + } + authenticator, err := lookupAuthenticator( clientset, flags.concierge.authenticatorType, @@ -186,7 +194,8 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar if err != nil { return err } - if err := configureConcierge(authenticator, &flags, cluster, &oidcCABundle, &execConfig); err != nil { + + if err := configureConcierge(credentialIssuer, authenticator, &flags, cluster, &oidcCABundle, &execConfig); err != nil { return err } } @@ -209,7 +218,7 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar // Otherwise continue to parse the OIDC-related flags and output a config that runs `pinniped login oidc`. execConfig.Args = append([]string{"login", "oidc"}, execConfig.Args...) if flags.oidc.issuer == "" { - return fmt.Errorf("could not autodiscover --oidc-issuer, and none was provided") + return fmt.Errorf("could not autodiscover --oidc-issuer and none was provided") } execConfig.Args = append(execConfig.Args, "--issuer="+flags.oidc.issuer, @@ -237,18 +246,30 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar return writeConfigAsYAML(out, newExecKubeconfig(cluster, &execConfig)) } -func configureConcierge(authenticator metav1.Object, flags *getKubeconfigParams, v1Cluster *clientcmdapi.Cluster, oidcCABundle *string, execConfig *clientcmdapi.ExecConfig) error { +func configureConcierge(credentialIssuer *configv1alpha1.CredentialIssuer, authenticator metav1.Object, flags *getKubeconfigParams, v1Cluster *clientcmdapi.Cluster, oidcCABundle *string, execConfig *clientcmdapi.ExecConfig) error { + var conciergeCABundleData []byte - if flags.concierge.mode == modeImpersonationProxy { - // TODO what to do if --use-impersonation-proxy is set but flags.concierge.caBundlePath is not??? - // TODO dont do this twice - conciergeCaBundleData, err := loadCABundlePaths([]string{flags.concierge.caBundlePath}) - if err != nil { - return fmt.Errorf("could not read --concierge-ca-bundle: %w", err) + // Autodiscover the --concierge-mode. + if flags.concierge.mode == modeUnknown { + + if credentialIssuer.Status.KubeConfigInfo != nil { + // Prefer the TokenCredentialRequest API if available. + flags.concierge.mode = modeTokenCredentialRequestAPI + } else if credentialIssuer.Status.ImpersonationProxyInfo != nil { + // Otherwise prefer the impersonation proxy if it seems configured. + flags.concierge.mode = modeImpersonationProxy + } else { + return fmt.Errorf("could not autodiscover --concierge-mode and none was provided") } + } - v1Cluster.CertificateAuthorityData = []byte(conciergeCaBundleData) - v1Cluster.Server = flags.concierge.endpoint + if flags.concierge.mode == modeImpersonationProxy && credentialIssuer.Status.ImpersonationProxyInfo != nil { + flags.concierge.endpoint = credentialIssuer.Status.ImpersonationProxyInfo.Endpoint + var err error + conciergeCABundleData, err = base64.StdEncoding.DecodeString(credentialIssuer.Status.ImpersonationProxyInfo.CertificateAuthorityData) + if err != nil { + return fmt.Errorf("autodiscovered Concierge CA bundle is invalid: %w", err) + } } switch auth := authenticator.(type) { @@ -292,15 +313,16 @@ func configureConcierge(authenticator metav1.Object, flags *getKubeconfigParams, flags.concierge.endpoint = v1Cluster.Server } - var encodedConciergeCaBundleData string - if flags.concierge.caBundlePath == "" { - encodedConciergeCaBundleData = base64.StdEncoding.EncodeToString(v1Cluster.CertificateAuthorityData) - } else { - conciergeCaBundleData, err := loadCABundlePaths([]string{flags.concierge.caBundlePath}) - if err != nil { - return fmt.Errorf("could not read --concierge-ca-bundle: %w", err) + if conciergeCABundleData == nil { + if flags.concierge.caBundlePath == "" { + conciergeCABundleData = v1Cluster.CertificateAuthorityData + } else { + caBundleString, err := loadCABundlePaths([]string{flags.concierge.caBundlePath}) + if err != nil { + return fmt.Errorf("could not read --concierge-ca-bundle: %w", err) + } + conciergeCABundleData = []byte(caBundleString) } - encodedConciergeCaBundleData = base64.StdEncoding.EncodeToString([]byte(conciergeCaBundleData)) } // Append the flags to configure the Concierge credential exchange at runtime. @@ -310,9 +332,16 @@ func configureConcierge(authenticator metav1.Object, flags *getKubeconfigParams, "--concierge-authenticator-name="+flags.concierge.authenticatorName, "--concierge-authenticator-type="+flags.concierge.authenticatorType, "--concierge-endpoint="+flags.concierge.endpoint, - "--concierge-ca-bundle-data="+encodedConciergeCaBundleData, + "--concierge-ca-bundle-data="+base64.StdEncoding.EncodeToString(conciergeCABundleData), "--concierge-mode="+flags.concierge.mode.String(), ) + + // If we're in impersonation proxy mode, the main server endpoint for the kubeconfig also needs to point to the proxy + if flags.concierge.mode == modeImpersonationProxy { + v1Cluster.CertificateAuthorityData = conciergeCABundleData + v1Cluster.Server = flags.concierge.endpoint + } + return nil } @@ -343,6 +372,29 @@ func newExecKubeconfig(cluster *clientcmdapi.Cluster, execConfig *clientcmdapi.E } } +func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string) (*configv1alpha1.CredentialIssuer, error) { + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20) + defer cancelFunc() + + // If the name is specified, get that object. + if name != "" { + return clientset.ConfigV1alpha1().CredentialIssuers().Get(ctx, name, metav1.GetOptions{}) + } + + // Otherwise list all the available CredentialIssuers and hope there's just a single one + results, err := clientset.ConfigV1alpha1().CredentialIssuers().List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to list CredentialIssuer objects for autodiscovery: %w", err) + } + if len(results.Items) == 0 { + return nil, fmt.Errorf("no CredentialIssuers were found") + } + if len(results.Items) > 1 { + return nil, fmt.Errorf("multiple CredentialIssuers were found, so the --concierge-credential-issuer flag must be specified") + } + return &results.Items[0], nil +} + func lookupAuthenticator(clientset conciergeclientset.Interface, authType, authName string) (metav1.Object, error) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20) defer cancelFunc() diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index 2a431c1e..94bb5017 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -20,6 +20,7 @@ import ( "k8s.io/client-go/tools/clientcmd" conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" + configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" fakeconciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake" "go.pinniped.dev/internal/certauthority" @@ -65,6 +66,7 @@ func TestGetKubeconfig(t *testing.T) { --concierge-authenticator-name string Concierge authenticator name (default: autodiscover) --concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover) --concierge-ca-bundle string Path to TLS certificate authority bundle (PEM format, optional, can be repeated) to use when connecting to the Concierge + --concierge-credential-issuer string Concierge CredentialIssuer object to use for autodiscovery (default: autodiscover) --concierge-endpoint string API base for the Concierge endpoint --concierge-mode mode Concierge mode of operation (default TokenCredentialRequestAPI) -h, --help help for kubeconfig @@ -134,6 +136,31 @@ func TestGetKubeconfig(t *testing.T) { Error: could not configure Kubernetes client: some kube error `), }, + { + name: "no credentialissuers", + args: []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + }, + wantError: true, + wantStderr: here.Doc(` + Error: no CredentialIssuers were found + `), + }, + + { + name: "credentialissuer not found", + args: []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--concierge-credential-issuer", "does-not-exist", + }, + conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + }, + wantError: true, + wantStderr: here.Doc(` + Error: credentialissuers.config.concierge.pinniped.dev "does-not-exist" not found + `), + }, { name: "webhook authenticator not found", args: []string{ @@ -141,6 +168,9 @@ func TestGetKubeconfig(t *testing.T) { "--concierge-authenticator-type", "webhook", "--concierge-authenticator-name", "test-authenticator", }, + conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + }, wantError: true, wantStderr: here.Doc(` Error: webhookauthenticators.authentication.concierge.pinniped.dev "test-authenticator" not found @@ -153,6 +183,9 @@ func TestGetKubeconfig(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", "test-authenticator", }, + conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + }, wantError: true, wantStderr: here.Doc(` Error: jwtauthenticators.authentication.concierge.pinniped.dev "test-authenticator" not found @@ -165,6 +198,9 @@ func TestGetKubeconfig(t *testing.T) { "--concierge-authenticator-type", "invalid", "--concierge-authenticator-name", "test-authenticator", }, + conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + }, wantError: true, wantStderr: here.Doc(` Error: invalid authenticator type "invalid", supported values are "webhook" and "jwt" @@ -175,6 +211,9 @@ func TestGetKubeconfig(t *testing.T) { args: []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", }, + conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + }, conciergeReactions: []kubetesting.Reactor{ &kubetesting.SimpleReactor{ Verb: "*", @@ -194,6 +233,9 @@ func TestGetKubeconfig(t *testing.T) { args: []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", }, + conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + }, conciergeReactions: []kubetesting.Reactor{ &kubetesting.SimpleReactor{ Verb: "*", @@ -213,6 +255,9 @@ func TestGetKubeconfig(t *testing.T) { args: []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", }, + conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + }, wantError: true, wantStderr: here.Doc(` Error: no authenticators were found @@ -224,6 +269,7 @@ func TestGetKubeconfig(t *testing.T) { "--kubeconfig", "./testdata/kubeconfig.yaml", }, conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, &conciergev1alpha1.JWTAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-1"}}, &conciergev1alpha1.JWTAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-2"}}, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-3"}}, @@ -234,17 +280,62 @@ func TestGetKubeconfig(t *testing.T) { Error: multiple authenticators were found, so the --concierge-authenticator-type/--concierge-authenticator-name flags must be specified `), }, + { + name: "autodetect webhook authenticator, bad credential issuer with no status", + args: []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + }, + conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, + }, + wantError: true, + wantStderr: here.Doc(` + Error: could not autodiscover --concierge-mode and none was provided + `), + }, + { + name: "autodetect webhook authenticator, bad credential issuer with invalid impersonation CA", + args: []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + }, + conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{ + ImpersonationProxyInfo: &configv1alpha1.CredentialIssuerImpersonationProxyInfo{ + Endpoint: "https://impersonation-endpoint", + CertificateAuthorityData: "invalid-base-64", + }, + }, + }, + &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, + }, + wantError: true, + wantStderr: here.Doc(` + Error: autodiscovered Concierge CA bundle is invalid: illegal base64 data at input byte 7 + `), + }, { name: "autodetect webhook authenticator, missing --oidc-issuer", args: []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", }, conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{ + KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ + Server: "https://concierge-endpoint", + CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", + }, + }, + }, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, }, wantError: true, wantStderr: here.Doc(` - Error: could not autodiscover --oidc-issuer, and none was provided + Error: could not autodiscover --oidc-issuer and none was provided `), }, { @@ -253,6 +344,15 @@ func TestGetKubeconfig(t *testing.T) { "--kubeconfig", "./testdata/kubeconfig.yaml", }, conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{ + KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ + Server: "https://concierge-endpoint", + CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", + }, + }, + }, &conciergev1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, Spec: conciergev1alpha1.JWTAuthenticatorSpec{ @@ -268,7 +368,7 @@ func TestGetKubeconfig(t *testing.T) { `), }, { - name: " invalid concierge ca bundle", + name: "invalid concierge ca bundle", args: []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", "--concierge-ca-bundle", "./does/not/exist", @@ -278,6 +378,15 @@ func TestGetKubeconfig(t *testing.T) { "--concierge-mode", "ImpersonationProxy", }, conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{ + KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ + Server: "https://concierge-endpoint", + CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", + }, + }, + }, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, }, wantError: true, @@ -293,6 +402,15 @@ func TestGetKubeconfig(t *testing.T) { "--static-token-env", "TEST_TOKEN", }, conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{ + KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ + Server: "https://concierge-endpoint", + CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", + }, + }, + }, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, }, wantError: true, @@ -317,6 +435,15 @@ func TestGetKubeconfig(t *testing.T) { "--static-token", "test-token", }, conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{ + KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ + Server: "https://concierge-endpoint", + CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", + }, + }, + }, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, }, wantStdout: here.Doc(` @@ -362,6 +489,15 @@ func TestGetKubeconfig(t *testing.T) { "--static-token-env", "TEST_TOKEN", }, conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{ + KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ + Server: "https://concierge-endpoint", + CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", + }, + }, + }, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, }, wantStdout: here.Doc(` @@ -406,6 +542,15 @@ func TestGetKubeconfig(t *testing.T) { "--kubeconfig", "./testdata/kubeconfig.yaml", }, conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{ + KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ + Server: "https://concierge-endpoint", + CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", + }, + }, + }, &conciergev1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, Spec: conciergev1alpha1.JWTAuthenticatorSpec{ @@ -473,6 +618,15 @@ func TestGetKubeconfig(t *testing.T) { "--oidc-request-audience", "test-audience", }, conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{ + KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ + Server: "https://concierge-endpoint", + CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", + }, + }, + }, &conciergev1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, }, @@ -531,6 +685,76 @@ func TestGetKubeconfig(t *testing.T) { "--concierge-mode", "ImpersonationProxy", }, conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{}, + }, + &conciergev1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, + Spec: conciergev1alpha1.JWTAuthenticatorSpec{ + Issuer: "https://example.com/issuer", + Audience: "test-audience", + TLS: &conciergev1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString(testOIDCCA.Bundle()), + }, + }, + }, + }, + wantStdout: here.Docf(` + apiVersion: v1 + clusters: + - cluster: + certificate-authority-data: dGVzdC1jb25jaWVyZ2UtY2E= + server: https://impersonation-proxy-endpoint.test + name: pinniped + contexts: + - context: + cluster: pinniped + user: pinniped + name: pinniped + current-context: pinniped + kind: Config + preferences: {} + users: + - name: pinniped + user: + exec: + apiVersion: client.authentication.k8s.io/v1beta1 + args: + - login + - oidc + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://impersonation-proxy-endpoint.test + - --concierge-ca-bundle-data=dGVzdC1jb25jaWVyZ2UtY2E= + - --concierge-mode=ImpersonationProxy + - --issuer=https://example.com/issuer + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --request-audience=test-audience + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, base64.StdEncoding.EncodeToString(testOIDCCA.Bundle())), + }, + { + name: "autodetect impersonation proxy with autodetected JWT authenticator", + args: []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + }, + conciergeObjects: []runtime.Object{ + &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{ + ImpersonationProxyInfo: &configv1alpha1.CredentialIssuerImpersonationProxyInfo{ + Endpoint: "https://impersonation-proxy-endpoint.test", + CertificateAuthorityData: "dGVzdC1jb25jaWVyZ2UtY2E=", + }, + }, + }, &conciergev1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, Spec: conciergev1alpha1.JWTAuthenticatorSpec{ @@ -604,7 +828,7 @@ func TestGetKubeconfig(t *testing.T) { } fake := fakeconciergeclientset.NewSimpleClientset(tt.conciergeObjects...) if len(tt.conciergeReactions) > 0 { - fake.ReactionChain = tt.conciergeReactions + fake.ReactionChain = append(tt.conciergeReactions, fake.ReactionChain...) } return fake, nil }, diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index 2eddee79..e1db5689 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -190,7 +190,7 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin // The exact behavior depends on in which mode the Concierge is operating. switch flags.conciergeMode { - case modeTokenCredentialRequestAPI: + case modeUnknown, modeTokenCredentialRequestAPI: // do a credential exchange request cred, err := deps.exchangeToken(ctx, concierge, token.IDToken.Token) if err != nil { diff --git a/cmd/pinniped/cmd/login_static.go b/cmd/pinniped/cmd/login_static.go index fd1526c2..9141c4e6 100644 --- a/cmd/pinniped/cmd/login_static.go +++ b/cmd/pinniped/cmd/login_static.go @@ -122,7 +122,7 @@ func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams // If the concierge is enabled, we need to do extra steps. switch flags.conciergeMode { - case modeTokenCredentialRequestAPI: + case modeUnknown, modeTokenCredentialRequestAPI: // do a credential exchange request ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel()