c82f568b2c
We were previously issuing both client certs and server certs with both extended key usages included. Split the Issue*() methods into separate methods for issuing server certs versus client certs so they can have different extended key usages tailored for each use case. Also took the opportunity to clean up the parameters of the Issue*() methods and New() methods to more closely match how we prefer to call them. We were always only passing the common name part of the pkix.Name to New(), so now the New() method just takes the common name as a string. When making a server cert, we don't need to set the deprecated common name field, so remove that param. When making a client cert, we're always making it in the format expected by the Kube API server, so just accept the username and group as parameters directly.
280 lines
8.9 KiB
Go
280 lines
8.9 KiB
Go
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package conciergeclient
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"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 := ioutil.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)
|
|
})
|
|
}
|