c6c2c525a6
Also fix some tests that were broken by bumping golang and dependencies in the previous commits. Note that in addition to changes made to satisfy the linter which do not impact the behavior of the code, this commit also adds ReadHeaderTimeout to all usages of http.Server to satisfy the linter (and because it seemed like a good suggestion).
280 lines
8.9 KiB
Go
280 lines
8.9 KiB
Go
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package conciergeclient
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
|
|
|
loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
|
|
"go.pinniped.dev/internal/certauthority"
|
|
"go.pinniped.dev/internal/testutil"
|
|
)
|
|
|
|
func TestNew(t *testing.T) {
|
|
t.Parallel()
|
|
testCA, err := certauthority.New("Test CA", 1*time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
opts []Option
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "some option error",
|
|
opts: []Option{
|
|
func(client *Client) error { return fmt.Errorf("some error") },
|
|
},
|
|
wantErr: "some error",
|
|
},
|
|
{
|
|
name: "with invalid authenticator",
|
|
opts: []Option{
|
|
WithAuthenticator("invalid-type", "test-authenticator"),
|
|
},
|
|
wantErr: `invalid authenticator type: "invalid-type", supported values are "webhook" and "jwt"`,
|
|
},
|
|
{
|
|
name: "with empty authenticator name",
|
|
opts: []Option{
|
|
WithAuthenticator("webhook", ""),
|
|
},
|
|
wantErr: `authenticator name must not be empty`,
|
|
},
|
|
{
|
|
name: "invalid CA bundle",
|
|
opts: []Option{
|
|
WithCABundle("invalid-base64"),
|
|
},
|
|
wantErr: "invalid CA bundle data: no certificates found",
|
|
},
|
|
{
|
|
name: "invalid base64 CA bundle",
|
|
opts: []Option{
|
|
WithBase64CABundle("invalid-base64"),
|
|
},
|
|
wantErr: "invalid CA bundle data: illegal base64 data at input byte 7",
|
|
},
|
|
{
|
|
name: "empty endpoint",
|
|
opts: []Option{
|
|
WithEndpoint(""),
|
|
},
|
|
wantErr: `endpoint must not be empty`,
|
|
},
|
|
{
|
|
name: "invalid endpoint",
|
|
opts: []Option{
|
|
WithEndpoint("%"),
|
|
},
|
|
wantErr: `invalid endpoint URL: parse "%": invalid URL escape "%"`,
|
|
},
|
|
{
|
|
name: "non-https endpoint",
|
|
opts: []Option{
|
|
WithEndpoint("http://example.com"),
|
|
},
|
|
wantErr: `invalid endpoint scheme "http" (must be "https")`,
|
|
},
|
|
{
|
|
name: "missing authenticator",
|
|
opts: []Option{
|
|
WithEndpoint("https://example.com"),
|
|
},
|
|
wantErr: "WithAuthenticator must be specified",
|
|
},
|
|
{
|
|
name: "missing endpoint",
|
|
opts: []Option{
|
|
WithAuthenticator("jwt", "test-authenticator"),
|
|
},
|
|
wantErr: "WithEndpoint must be specified",
|
|
},
|
|
{
|
|
name: "empty api group suffix",
|
|
opts: []Option{
|
|
WithAuthenticator("jwt", "test-authenticator"),
|
|
WithEndpoint("https://example.com"),
|
|
WithAPIGroupSuffix(""),
|
|
},
|
|
wantErr: "invalid API group suffix: [must contain '.', a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]",
|
|
},
|
|
{
|
|
name: "invalid API group suffix",
|
|
opts: []Option{
|
|
WithAuthenticator("jwt", "test-authenticator"),
|
|
WithEndpoint("https://example.com"),
|
|
WithAPIGroupSuffix(".starts.with.dot"),
|
|
},
|
|
wantErr: "invalid API group suffix: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')",
|
|
},
|
|
{
|
|
name: "valid",
|
|
opts: []Option{
|
|
WithEndpoint("https://example.com"),
|
|
WithCABundle(""),
|
|
WithCABundle(string(testCA.Bundle())),
|
|
WithBase64CABundle(base64.StdEncoding.EncodeToString(testCA.Bundle())),
|
|
WithAuthenticator("jwt", "test-authenticator"),
|
|
WithAuthenticator("webhook", "test-authenticator"),
|
|
WithAPIGroupSuffix("suffix.com"),
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got, err := New(tt.opts...)
|
|
if tt.wantErr != "" {
|
|
require.EqualError(t, err, tt.wantErr)
|
|
require.Nil(t, got)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.NotNil(t, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExchangeToken(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
t.Run("clientset failure", func(t *testing.T) {
|
|
c := Client{endpoint: &url.URL{}}
|
|
_, err := c.ExchangeToken(ctx, "")
|
|
require.EqualError(t, err, "invalid configuration: no configuration has been provided, try setting KUBERNETES_MASTER environment variable")
|
|
})
|
|
|
|
t.Run("server error", func(t *testing.T) {
|
|
t.Parallel()
|
|
// Start a test server that returns only 500 errors.
|
|
caBundle, endpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte("some server error"))
|
|
})
|
|
|
|
client, err := New(WithEndpoint(endpoint), WithCABundle(caBundle), WithAuthenticator("jwt", "test-authenticator"))
|
|
require.NoError(t, err)
|
|
|
|
got, err := client.ExchangeToken(ctx, "test-token")
|
|
require.EqualError(t, err, `could not login: an error on the server ("some server error") has prevented the request from succeeding (post tokencredentialrequests.login.concierge.pinniped.dev)`)
|
|
require.Nil(t, got)
|
|
})
|
|
|
|
t.Run("login failure", func(t *testing.T) {
|
|
t.Parallel()
|
|
// Start a test server that returns success but with an error message
|
|
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(&loginv1alpha1.TokenCredentialRequest{
|
|
TypeMeta: metav1.TypeMeta{APIVersion: "login.concierge.pinniped.dev/v1alpha1", Kind: "TokenCredentialRequest"},
|
|
Status: loginv1alpha1.TokenCredentialRequestStatus{Message: &errorMessage},
|
|
})
|
|
})
|
|
|
|
client, err := New(WithEndpoint(endpoint), WithCABundle(caBundle), WithAuthenticator("jwt", "test-authenticator"))
|
|
require.NoError(t, err)
|
|
|
|
got, err := client.ExchangeToken(ctx, "test-token")
|
|
require.EqualError(t, err, `login failed: some login failure`)
|
|
require.Nil(t, got)
|
|
})
|
|
|
|
t.Run("login failure unknown error", func(t *testing.T) {
|
|
t.Parallel()
|
|
// 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(&loginv1alpha1.TokenCredentialRequest{
|
|
TypeMeta: metav1.TypeMeta{APIVersion: "login.concierge.pinniped.dev/v1alpha1", Kind: "TokenCredentialRequest"},
|
|
})
|
|
})
|
|
|
|
client, err := New(WithEndpoint(endpoint), WithCABundle(caBundle), WithAuthenticator("jwt", "test-authenticator"))
|
|
require.NoError(t, err)
|
|
|
|
got, err := client.ExchangeToken(ctx, "test-token")
|
|
require.EqualError(t, err, `login failed: unknown cause`)
|
|
require.Nil(t, got)
|
|
})
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
t.Parallel()
|
|
expires := metav1.NewTime(time.Now().Truncate(time.Second))
|
|
|
|
// Start a test server that returns successfully and asserts various properties of the request.
|
|
caBundle, endpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, http.MethodPost, r.Method)
|
|
require.Equal(t, "/apis/login.concierge.pinniped.dev/v1alpha1/tokencredentialrequests", r.URL.Path)
|
|
require.Equal(t, "application/json", r.Header.Get("content-type"))
|
|
|
|
body, err := io.ReadAll(r.Body)
|
|
require.NoError(t, err)
|
|
require.JSONEq(t,
|
|
`{
|
|
"kind": "TokenCredentialRequest",
|
|
"apiVersion": "login.concierge.pinniped.dev/v1alpha1",
|
|
"metadata": {
|
|
"creationTimestamp": null
|
|
},
|
|
"spec": {
|
|
"token": "test-token",
|
|
"authenticator": {
|
|
"apiGroup": "authentication.concierge.pinniped.dev",
|
|
"kind": "WebhookAuthenticator",
|
|
"name": "test-webhook"
|
|
}
|
|
},
|
|
"status": {}
|
|
}`,
|
|
string(body),
|
|
)
|
|
|
|
w.Header().Set("content-type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(&loginv1alpha1.TokenCredentialRequest{
|
|
TypeMeta: metav1.TypeMeta{APIVersion: "login.concierge.pinniped.dev/v1alpha1", Kind: "TokenCredentialRequest"},
|
|
Status: loginv1alpha1.TokenCredentialRequestStatus{
|
|
Credential: &loginv1alpha1.ClusterCredential{
|
|
ExpirationTimestamp: expires,
|
|
ClientCertificateData: "test-certificate",
|
|
ClientKeyData: "test-key",
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
client, err := New(WithEndpoint(endpoint), WithCABundle(caBundle), WithAuthenticator("webhook", "test-webhook"))
|
|
require.NoError(t, err)
|
|
|
|
got, err := client.ExchangeToken(ctx, "test-token")
|
|
require.NoError(t, err)
|
|
require.Equal(t, &clientauthenticationv1beta1.ExecCredential{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "ExecCredential",
|
|
APIVersion: "client.authentication.k8s.io/v1beta1",
|
|
},
|
|
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
|
ClientCertificateData: "test-certificate",
|
|
ClientKeyData: "test-key",
|
|
ExpirationTimestamp: &expires,
|
|
},
|
|
}, got)
|
|
})
|
|
}
|