Add IDP selector support in client code.
Signed-off-by: Matt Moyer <moyerm@vmware.com>
This commit is contained in:
parent
164f64a370
commit
fbe0551426
@ -9,11 +9,14 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
|
||||
idpv1alpha1 "go.pinniped.dev/generated/1.19/apis/idp/v1alpha1"
|
||||
"go.pinniped.dev/internal/client"
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/here"
|
||||
@ -75,9 +78,19 @@ func newExchangeCredentialCmd(args []string, stdout, stderr io.Writer) *exchange
|
||||
}
|
||||
|
||||
type envGetter func(string) (string, bool)
|
||||
type tokenExchanger func(ctx context.Context, namespace, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error)
|
||||
type tokenExchanger func(
|
||||
ctx context.Context,
|
||||
namespace string,
|
||||
idp corev1.TypedLocalObjectReference,
|
||||
token string,
|
||||
caBundle string,
|
||||
apiEndpoint string,
|
||||
) (*clientauthenticationv1beta1.ExecCredential, error)
|
||||
|
||||
const ErrMissingEnvVar = constable.Error("failed to get credential: environment variable not set")
|
||||
const (
|
||||
ErrMissingEnvVar = constable.Error("failed to get credential: environment variable not set")
|
||||
ErrInvalidIDPType = constable.Error("invalid IDP type")
|
||||
)
|
||||
|
||||
func runExchangeCredential(stdout, _ io.Writer) {
|
||||
err := exchangeCredential(os.LookupEnv, client.ExchangeToken, stdout, 30*time.Second)
|
||||
@ -96,6 +109,16 @@ func exchangeCredential(envGetter envGetter, tokenExchanger tokenExchanger, outp
|
||||
return envVarNotSetError("PINNIPED_NAMESPACE")
|
||||
}
|
||||
|
||||
idpType, varExists := envGetter("PINNIPED_IDP_TYPE")
|
||||
if !varExists {
|
||||
return envVarNotSetError("PINNIPED_IDP_TYPE")
|
||||
}
|
||||
|
||||
idpName, varExists := envGetter("PINNIPED_IDP_NAME")
|
||||
if !varExists {
|
||||
return envVarNotSetError("PINNIPED_IDP_NAME")
|
||||
}
|
||||
|
||||
token, varExists := envGetter("PINNIPED_TOKEN")
|
||||
if !varExists {
|
||||
return envVarNotSetError("PINNIPED_TOKEN")
|
||||
@ -111,7 +134,16 @@ func exchangeCredential(envGetter envGetter, tokenExchanger tokenExchanger, outp
|
||||
return envVarNotSetError("PINNIPED_K8S_API_ENDPOINT")
|
||||
}
|
||||
|
||||
cred, err := tokenExchanger(ctx, namespace, token, caBundle, apiEndpoint)
|
||||
idp := corev1.TypedLocalObjectReference{Name: idpName}
|
||||
switch strings.ToLower(idpType) {
|
||||
case "webhook":
|
||||
idp.APIGroup = &idpv1alpha1.SchemeGroupVersion.Group
|
||||
idp.Kind = "WebhookIdentityProvider"
|
||||
default:
|
||||
return fmt.Errorf(`%w: %q, supported values are "webhook"`, ErrInvalidIDPType, idpType)
|
||||
}
|
||||
|
||||
cred, err := tokenExchanger(ctx, namespace, idp, token, caBundle, apiEndpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get credential: %w", err)
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/sclevine/spec"
|
||||
"github.com/sclevine/spec/report"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
|
||||
@ -136,6 +137,8 @@ func TestExchangeCredential(t *testing.T) {
|
||||
buffer = new(bytes.Buffer)
|
||||
fakeEnv = map[string]string{
|
||||
"PINNIPED_NAMESPACE": "namespace from env",
|
||||
"PINNIPED_IDP_TYPE": "Webhook",
|
||||
"PINNIPED_IDP_NAME": "webhook name from env",
|
||||
"PINNIPED_TOKEN": "token from env",
|
||||
"PINNIPED_CA_BUNDLE": "ca bundle from env",
|
||||
"PINNIPED_K8S_API_ENDPOINT": "k8s api from env",
|
||||
@ -149,6 +152,18 @@ func TestExchangeCredential(t *testing.T) {
|
||||
r.EqualError(err, "failed to get credential: environment variable not set: PINNIPED_NAMESPACE")
|
||||
})
|
||||
|
||||
it("returns an error when PINNIPED_IDP_TYPE is missing", func() {
|
||||
delete(fakeEnv, "PINNIPED_IDP_TYPE")
|
||||
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||
r.EqualError(err, "failed to get credential: environment variable not set: PINNIPED_IDP_TYPE")
|
||||
})
|
||||
|
||||
it("returns an error when PINNIPED_IDP_NAME is missing", func() {
|
||||
delete(fakeEnv, "PINNIPED_IDP_NAME")
|
||||
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||
r.EqualError(err, "failed to get credential: environment variable not set: PINNIPED_IDP_NAME")
|
||||
})
|
||||
|
||||
it("returns an error when PINNIPED_TOKEN is missing", func() {
|
||||
delete(fakeEnv, "PINNIPED_TOKEN")
|
||||
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||
@ -168,9 +183,17 @@ func TestExchangeCredential(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
when("env vars are invalid", func() {
|
||||
it("returns an error when PINNIPED_IDP_TYPE is missing", func() {
|
||||
fakeEnv["PINNIPED_IDP_TYPE"] = "invalid"
|
||||
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||
r.EqualError(err, `invalid IDP type: "invalid", supported values are "webhook"`)
|
||||
})
|
||||
})
|
||||
|
||||
when("the token exchange fails", func() {
|
||||
it.Before(func() {
|
||||
tokenExchanger = func(ctx context.Context, namespace, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||
tokenExchanger = func(ctx context.Context, namespace string, idp corev1.TypedLocalObjectReference, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||
return nil, fmt.Errorf("some error")
|
||||
}
|
||||
})
|
||||
@ -183,7 +206,7 @@ func TestExchangeCredential(t *testing.T) {
|
||||
|
||||
when("the JSON encoder fails", func() {
|
||||
it.Before(func() {
|
||||
tokenExchanger = func(ctx context.Context, namespace, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||
tokenExchanger = func(ctx context.Context, namespace string, idp corev1.TypedLocalObjectReference, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||
return &clientauthenticationv1beta1.ExecCredential{
|
||||
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
||||
Token: "some token",
|
||||
@ -200,7 +223,7 @@ func TestExchangeCredential(t *testing.T) {
|
||||
|
||||
when("the token exchange times out", func() {
|
||||
it.Before(func() {
|
||||
tokenExchanger = func(ctx context.Context, namespace, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||
tokenExchanger = func(ctx context.Context, namespace string, idp corev1.TypedLocalObjectReference, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||
select {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
return &clientauthenticationv1beta1.ExecCredential{
|
||||
@ -224,7 +247,7 @@ func TestExchangeCredential(t *testing.T) {
|
||||
var actualNamespace, actualToken, actualCaBundle, actualAPIEndpoint string
|
||||
|
||||
it.Before(func() {
|
||||
tokenExchanger = func(ctx context.Context, namespace, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||
tokenExchanger = func(ctx context.Context, namespace string, idp corev1.TypedLocalObjectReference, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||
actualNamespace, actualToken, actualCaBundle, actualAPIEndpoint = namespace, token, caBundle, apiEndpoint
|
||||
now := metav1.NewTime(time.Date(2020, 7, 29, 1, 2, 3, 0, time.UTC))
|
||||
return &clientauthenticationv1beta1.ExecCredential{
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
@ -21,15 +22,18 @@ import (
|
||||
// ErrLoginFailed is returned by ExchangeToken when the server rejects the login request.
|
||||
var ErrLoginFailed = errors.New("login failed")
|
||||
|
||||
// ExchangeToken exchanges an opaque token using the Pinniped CredentialRequest API, returning a client-go ExecCredential valid on the target cluster.
|
||||
func ExchangeToken(ctx context.Context, namespace string, token string, caBundle string, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||
// ExchangeToken exchanges an opaque token using the Pinniped TokenCredentialRequest API, returning a client-go ExecCredential valid on the target cluster.
|
||||
func ExchangeToken(ctx context.Context, namespace string, idp corev1.TypedLocalObjectReference, token string, caBundle string, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||
client, err := getClient(apiEndpoint, caBundle)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get API client: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.LoginV1alpha1().TokenCredentialRequests(namespace).Create(ctx, &v1alpha1.TokenCredentialRequest{
|
||||
Spec: v1alpha1.TokenCredentialRequestSpec{Token: token},
|
||||
Spec: v1alpha1.TokenCredentialRequestSpec{
|
||||
Token: token,
|
||||
IdentityProvider: idp,
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not login: %w", err)
|
||||
|
@ -12,10 +12,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
|
||||
"go.pinniped.dev/generated/1.19/apis/login/v1alpha1"
|
||||
idpv1alpha1 "go.pinniped.dev/generated/1.19/apis/idp/v1alpha1"
|
||||
loginv1alpha1 "go.pinniped.dev/generated/1.19/apis/login/v1alpha1"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
)
|
||||
|
||||
@ -23,9 +25,15 @@ func TestExchangeToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
testIDP := corev1.TypedLocalObjectReference{
|
||||
APIGroup: &idpv1alpha1.SchemeGroupVersion.Group,
|
||||
Kind: "WebhookIdentityProvider",
|
||||
Name: "test-webhook",
|
||||
}
|
||||
|
||||
t.Run("invalid configuration", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got, err := ExchangeToken(ctx, "test-namespace", "", "", "")
|
||||
got, err := ExchangeToken(ctx, "test-namespace", testIDP, "", "", "")
|
||||
require.EqualError(t, err, "could not get API client: invalid configuration: no configuration has been provided, try setting KUBERNETES_MASTER environment variable")
|
||||
require.Nil(t, got)
|
||||
})
|
||||
@ -38,7 +46,7 @@ func TestExchangeToken(t *testing.T) {
|
||||
_, _ = w.Write([]byte("some server error"))
|
||||
})
|
||||
|
||||
got, err := ExchangeToken(ctx, "test-namespace", "", caBundle, endpoint)
|
||||
got, err := ExchangeToken(ctx, "test-namespace", testIDP, "", caBundle, endpoint)
|
||||
require.EqualError(t, err, `could not login: an error on the server ("some server error") has prevented the request from succeeding (post tokencredentialrequests.login.pinniped.dev)`)
|
||||
require.Nil(t, got)
|
||||
})
|
||||
@ -49,13 +57,13 @@ func TestExchangeToken(t *testing.T) {
|
||||
errorMessage := "some login failure"
|
||||
caBundle, endpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("content-type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(&v1alpha1.TokenCredentialRequest{
|
||||
_ = json.NewEncoder(w).Encode(&loginv1alpha1.TokenCredentialRequest{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "login.pinniped.dev/v1alpha1", Kind: "TokenCredentialRequest"},
|
||||
Status: v1alpha1.TokenCredentialRequestStatus{Message: &errorMessage},
|
||||
Status: loginv1alpha1.TokenCredentialRequestStatus{Message: &errorMessage},
|
||||
})
|
||||
})
|
||||
|
||||
got, err := ExchangeToken(ctx, "test-namespace", "", caBundle, endpoint)
|
||||
got, err := ExchangeToken(ctx, "test-namespace", testIDP, "", caBundle, endpoint)
|
||||
require.EqualError(t, err, `login failed: some login failure`)
|
||||
require.Nil(t, got)
|
||||
})
|
||||
@ -65,12 +73,12 @@ func TestExchangeToken(t *testing.T) {
|
||||
// Start a test server that returns without any error message but also without valid credentials
|
||||
caBundle, endpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("content-type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(&v1alpha1.TokenCredentialRequest{
|
||||
_ = json.NewEncoder(w).Encode(&loginv1alpha1.TokenCredentialRequest{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "login.pinniped.dev/v1alpha1", Kind: "TokenCredentialRequest"},
|
||||
})
|
||||
})
|
||||
|
||||
got, err := ExchangeToken(ctx, "test-namespace", "", caBundle, endpoint)
|
||||
got, err := ExchangeToken(ctx, "test-namespace", testIDP, "", caBundle, endpoint)
|
||||
require.EqualError(t, err, `login failed: unknown`)
|
||||
require.Nil(t, got)
|
||||
})
|
||||
@ -95,7 +103,12 @@ func TestExchangeToken(t *testing.T) {
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"spec": {
|
||||
"token": "test-token"
|
||||
"token": "test-token",
|
||||
"identityProvider": {
|
||||
"apiGroup": "idp.pinniped.dev",
|
||||
"kind": "WebhookIdentityProvider",
|
||||
"name": "test-webhook"
|
||||
}
|
||||
},
|
||||
"status": {}
|
||||
}`,
|
||||
@ -103,10 +116,10 @@ func TestExchangeToken(t *testing.T) {
|
||||
)
|
||||
|
||||
w.Header().Set("content-type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(&v1alpha1.TokenCredentialRequest{
|
||||
_ = json.NewEncoder(w).Encode(&loginv1alpha1.TokenCredentialRequest{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "login.pinniped.dev/v1alpha1", Kind: "TokenCredentialRequest"},
|
||||
Status: v1alpha1.TokenCredentialRequestStatus{
|
||||
Credential: &v1alpha1.ClusterCredential{
|
||||
Status: loginv1alpha1.TokenCredentialRequestStatus{
|
||||
Credential: &loginv1alpha1.ClusterCredential{
|
||||
ExpirationTimestamp: expires,
|
||||
ClientCertificateData: "test-certificate",
|
||||
ClientKeyData: "test-key",
|
||||
@ -115,7 +128,7 @@ func TestExchangeToken(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
got, err := ExchangeToken(ctx, "test-namespace", "test-token", caBundle, endpoint)
|
||||
got, err := ExchangeToken(ctx, "test-namespace", testIDP, "test-token", caBundle, endpoint)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &clientauthenticationv1beta1.ExecCredential{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
|
@ -10,7 +10,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
idpv1alpha1 "go.pinniped.dev/generated/1.19/apis/idp/v1alpha1"
|
||||
"go.pinniped.dev/internal/client"
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/test/library"
|
||||
@ -68,7 +70,13 @@ func TestClient(t *testing.T) {
|
||||
|
||||
// Using the CA bundle and host from the current (admin) kubeconfig, do the token exchange.
|
||||
clientConfig := library.NewClientConfig(t)
|
||||
resp, err := client.ExchangeToken(ctx, namespace, token, string(clientConfig.CAData), clientConfig.Host)
|
||||
|
||||
idp := corev1.TypedLocalObjectReference{
|
||||
APIGroup: &idpv1alpha1.SchemeGroupVersion.Group,
|
||||
Kind: "WebhookIdentityProvider",
|
||||
Name: "pinniped-webhook",
|
||||
}
|
||||
resp, err := client.ExchangeToken(ctx, namespace, idp, token, string(clientConfig.CAData), clientConfig.Host)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp.Status.ExpirationTimestamp)
|
||||
require.InDelta(t, time.Until(resp.Status.ExpirationTimestamp.Time), 1*time.Hour, float64(3*time.Minute))
|
||||
|
Loading…
Reference in New Issue
Block a user