diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index 8959370f6..a4e748831 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -115,7 +115,7 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command { f.StringVar(&flags.concierge.caBundleData, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge") f.StringVar(&flags.concierge.endpoint, "concierge-endpoint", "", "API base for the Pinniped concierge endpoint") - f.BoolVar(&flags.concierge.useImpersonationProxy, "use-impersonation-proxy", false, "Whether the concierge cluster uses an impersonation proxy") + f.BoolVar(&flags.concierge.useImpersonationProxy, "concierge-use-impersonation-proxy", false, "Whether the concierge cluster uses an impersonation proxy") f.StringVar(&flags.oidc.issuer, "oidc-issuer", "", "OpenID Connect issuer URL (default: autodiscover)") f.StringVar(&flags.oidc.clientID, "oidc-client-id", "pinniped-cli", "OpenID Connect client ID (default: autodiscover)") @@ -296,7 +296,7 @@ func configureConcierge(authenticator metav1.Object, flags *getKubeconfigParams, ) if flags.concierge.useImpersonationProxy { execConfig.Args = append(execConfig.Args, - "--use-impersonation-proxy", + "--concierge-use-impersonation-proxy", ) } return nil diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index 07e0d8c14..9c588adb7 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -64,6 +64,7 @@ func TestGetKubeconfig(t *testing.T) { --concierge-ca-bundle-data string CA bundle to use when connecting to the concierge --concierge-endpoint string API base for the Pinniped concierge endpoint --concierge-namespace string Namespace in which the concierge was installed (default "pinniped-concierge") + --concierge-use-impersonation-proxy Whether the concierge cluster uses an impersonation proxy -h, --help help for kubeconfig --kubeconfig string Path to kubeconfig file --kubeconfig-context string Kubeconfig context name (default: current active context) @@ -78,7 +79,6 @@ func TestGetKubeconfig(t *testing.T) { --oidc-skip-browser During OpenID Connect login, skip opening the browser (just print the URL) --static-token string Instead of doing an OIDC-based login, specify a static token --static-token-env string Instead of doing an OIDC-based login, read a static token from the environment - --use-impersonation-proxy Whether the concierge cluster uses an impersonation proxy `), }, { @@ -515,7 +515,7 @@ func TestGetKubeconfig(t *testing.T) { "--kubeconfig", "./testdata/kubeconfig.yaml", "--concierge-ca-bundle-data", "blah", // TODO make this more realistic, maybe do some validation? "--concierge-endpoint", "https://impersonation-proxy-endpoint.test", - "--use-impersonation-proxy", + "--concierge-use-impersonation-proxy", }, conciergeObjects: []runtime.Object{ &conciergev1alpha1.JWTAuthenticator{ @@ -559,7 +559,7 @@ func TestGetKubeconfig(t *testing.T) { - --concierge-authenticator-type=jwt - --concierge-endpoint=https://impersonation-proxy-endpoint.test - --concierge-ca-bundle-data=blah - - --use-impersonation-proxy + - --concierge-use-impersonation-proxy - --issuer=https://example.com/issuer - --client-id=pinniped-cli - --scopes=offline_access,openid,pinniped:request-audience diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index 831e4f01d..999fd8a7c 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -101,7 +101,7 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command { cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Pinniped concierge endpoint") cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge") cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", "pinniped.dev", "Concierge API group suffix") - cmd.Flags().BoolVar(&flags.useImpersonationProxy, "use-impersonation-proxy", false, "Whether the concierge cluster uses an impersonation proxy") + cmd.Flags().BoolVar(&flags.useImpersonationProxy, "concierge-use-impersonation-proxy", false, "Whether the concierge cluster uses an impersonation proxy") mustMarkHidden(&cmd, "debug-session-cache") mustMarkRequired(&cmd, "issuer") @@ -187,7 +187,8 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin } } if concierge != nil && flags.useImpersonationProxy { - // TODO add the right header??? + // Put the token into a TokenCredentialRequest + // put the TokenCredentialRequest in an ExecCredential req, err := execCredentialForImpersonationProxy(token, flags) if err != nil { return err diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go index ba02bd8d7..883cc31fd 100644 --- a/cmd/pinniped/cmd/login_oidc_test.go +++ b/cmd/pinniped/cmd/login_oidc_test.go @@ -72,6 +72,7 @@ func TestLoginOIDCCommand(t *testing.T) { --concierge-ca-bundle-data string CA bundle to use when connecting to the concierge --concierge-endpoint string API base for the Pinniped concierge endpoint --concierge-namespace string Namespace in which the concierge was installed (default "pinniped-concierge") + --concierge-use-impersonation-proxy Whether the concierge cluster uses an impersonation proxy --enable-concierge Exchange the OIDC ID token with the Pinniped concierge during login -h, --help help for oidc --issuer string OpenID Connect issuer URL @@ -80,7 +81,6 @@ func TestLoginOIDCCommand(t *testing.T) { --scopes strings OIDC scopes to request during login (default [offline_access,openid,pinniped:request-audience]) --session-cache string Path to session cache file (default "` + cfgDir + `/sessions.yaml") --skip-browser Skip opening the browser (just print the URL) - --use-impersonation-proxy Whether the concierge cluster uses an impersonation proxy `), }, { @@ -210,14 +210,14 @@ func TestLoginOIDCCommand(t *testing.T) { "--client-id", "test-client-id", "--issuer", "test-issuer", "--enable-concierge", - "--use-impersonation-proxy", + "--concierge-use-impersonation-proxy", "--concierge-authenticator-type", "webhook", "--concierge-authenticator-name", "test-authenticator", "--concierge-endpoint", "https://127.0.0.1:1234/", "--concierge-ca-bundle-data", base64.StdEncoding.EncodeToString(testCA.Bundle()), }, wantOptionsCount: 3, - wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"` + impersonationProxyToken("test-id-token") + `"}}` + "\n", + wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"` + impersonationProxyTestToken("test-id-token") + `"}}` + "\n", }, } for _, tt := range tests { @@ -276,7 +276,7 @@ func TestLoginOIDCCommand(t *testing.T) { } } -func impersonationProxyToken(token string) string { +func impersonationProxyTestToken(token string) string { reqJSON, _ := json.Marshal(&loginv1alpha1.TokenCredentialRequest{ ObjectMeta: metav1.ObjectMeta{ Namespace: "pinniped-concierge", diff --git a/cmd/pinniped/cmd/login_static.go b/cmd/pinniped/cmd/login_static.go index 52fcf095f..7477dfd4a 100644 --- a/cmd/pinniped/cmd/login_static.go +++ b/cmd/pinniped/cmd/login_static.go @@ -5,15 +5,22 @@ package cmd import ( "context" + "encoding/base64" "encoding/json" "fmt" "io" "os" + "strings" "time" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/spf13/cobra" clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" + authenticationv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/authentication/v1alpha1" + loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1" "go.pinniped.dev/pkg/conciergeclient" "go.pinniped.dev/pkg/oidcclient/oidctypes" ) @@ -47,6 +54,7 @@ type staticLoginParams struct { conciergeEndpoint string conciergeCABundle string conciergeAPIGroupSuffix string + useImpersonationProxy bool } func staticLoginCommand(deps staticLoginDeps) *cobra.Command { @@ -68,6 +76,7 @@ func staticLoginCommand(deps staticLoginDeps) *cobra.Command { cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Pinniped concierge endpoint") cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge") cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", "pinniped.dev", "Concierge API group suffix") + cmd.Flags().BoolVar(&flags.useImpersonationProxy, "concierge-use-impersonation-proxy", false, "Whether the concierge cluster uses an impersonation proxy") cmd.RunE = func(cmd *cobra.Command, args []string) error { return runStaticLogin(cmd.OutOrStdout(), deps, flags) } return &cmd } @@ -109,7 +118,7 @@ func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams cred := tokenCredential(&oidctypes.Token{IDToken: &oidctypes.IDToken{Token: token}}) // Exchange that token with the concierge, if configured. - if concierge != nil { + if concierge != nil && !flags.useImpersonationProxy { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -119,5 +128,58 @@ func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams return fmt.Errorf("could not complete concierge credential exchange: %w", err) } } + if concierge != nil && flags.useImpersonationProxy { + // Put the token into a TokenCredentialRequest + // put the TokenCredentialRequest in an ExecCredential + req, err := execCredentialForImpersonationProxyStatic(token, flags) + if err != nil { + return err + } + return json.NewEncoder(out).Encode(req) + } return json.NewEncoder(out).Encode(cred) } + +func execCredentialForImpersonationProxyStatic(token string, flags staticLoginParams) (*clientauthv1beta1.ExecCredential, error) { + // TODO maybe de-dup this with conciergeclient.go + var kind string + switch strings.ToLower(flags.conciergeAuthenticatorType) { + case "webhook": + kind = "WebhookAuthenticator" + case "jwt": + kind = "JWTAuthenticator" + default: + return nil, fmt.Errorf(`invalid authenticator type: %q, supported values are "webhook" and "jwt"`, kind) + } + reqJSON, err := json.Marshal(&loginv1alpha1.TokenCredentialRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: flags.conciergeNamespace, + }, + TypeMeta: metav1.TypeMeta{ + Kind: "TokenCredentialRequest", + APIVersion: loginv1alpha1.GroupName + "/v1alpha1", + }, + Spec: loginv1alpha1.TokenCredentialRequestSpec{ + Token: token, // TODO + Authenticator: corev1.TypedLocalObjectReference{ + APIGroup: &authenticationv1alpha1.SchemeGroupVersion.Group, + Kind: kind, + Name: flags.conciergeAuthenticatorName, + }, + }, + }) + if err != nil { + return nil, err + } + encodedToken := base64.RawURLEncoding.EncodeToString(reqJSON) + cred := &clientauthv1beta1.ExecCredential{ + TypeMeta: metav1.TypeMeta{ + Kind: "ExecCredential", + APIVersion: "client.authentication.k8s.io/v1beta1", + }, + Status: &clientauthv1beta1.ExecCredentialStatus{ + Token: encodedToken, + }, + } + return cred, nil +} diff --git a/cmd/pinniped/cmd/login_static_test.go b/cmd/pinniped/cmd/login_static_test.go index 0152dbd0b..a19f09090 100644 --- a/cmd/pinniped/cmd/login_static_test.go +++ b/cmd/pinniped/cmd/login_static_test.go @@ -57,6 +57,7 @@ func TestLoginStaticCommand(t *testing.T) { --concierge-ca-bundle-data string CA bundle to use when connecting to the concierge --concierge-endpoint string API base for the Pinniped concierge endpoint --concierge-namespace string Namespace in which the concierge was installed (default "pinniped-concierge") + --concierge-use-impersonation-proxy Whether the concierge cluster uses an impersonation proxy --enable-concierge Exchange the token with the Pinniped concierge during login -h, --help help for static --token string Static token to present during login @@ -153,6 +154,18 @@ func TestLoginStaticCommand(t *testing.T) { }, wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"token":"test-token"}}` + "\n", }, + { + name: "impersonation proxy success", + args: []string{ + "--enable-concierge", + "--concierge-use-impersonation-proxy", + "--token", "test-token", + "--concierge-endpoint", "https://127.0.0.1/", + "--concierge-authenticator-type", "webhook", + "--concierge-authenticator-name", "test-authenticator", + }, + wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"token":"` + impersonationProxyTestToken("test-token") + `"}}` + "\n", + }, } for _, tt := range tests { tt := tt