cd686ffdf3
This change updates the TLS config used by all pinniped components. There are no configuration knobs associated with this change. Thus this change tightens our static defaults. There are four TLS config levels: 1. Secure (TLS 1.3 only) 2. Default (TLS 1.2+ best ciphers that are well supported) 3. Default LDAP (TLS 1.2+ with less good ciphers) 4. Legacy (currently unused, TLS 1.2+ with all non-broken ciphers) Highlights per component: 1. pinniped CLI - uses "secure" config against KAS - uses "default" for all other connections 2. concierge - uses "secure" config as an aggregated API server - uses "default" config as a impersonation proxy API server - uses "secure" config against KAS - uses "default" config for JWT authenticater (mostly, see code) - no changes to webhook authenticater (see code) 3. supervisor - uses "default" config as a server - uses "secure" config against KAS - uses "default" config against OIDC IDPs - uses "default LDAP" config against LDAP IDPs Signed-off-by: Monis Khan <mok@vmware.com>
571 lines
19 KiB
Go
571 lines
19 KiB
Go
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package localuserauthenticator
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/crypto/bcrypt"
|
|
authenticationv1beta1 "k8s.io/api/authentication/v1beta1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
utilnet "k8s.io/apimachinery/pkg/util/net"
|
|
kubeinformers "k8s.io/client-go/informers"
|
|
corev1informers "k8s.io/client-go/informers/core/v1"
|
|
"k8s.io/client-go/kubernetes"
|
|
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
|
|
|
"go.pinniped.dev/internal/certauthority"
|
|
"go.pinniped.dev/internal/dynamiccert"
|
|
"go.pinniped.dev/internal/net/phttp"
|
|
)
|
|
|
|
func TestWebhook(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const namespace = "local-user-authenticator"
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
user, otherUser, colonUser, noGroupUser, oneGroupUser, passwordUndefinedUser, emptyPasswordUser, invalidPasswordHashUser, undefinedGroupsUser :=
|
|
"some-user", "other-user", "colon-user", "no-group-user", "one-group-user", "password-undefined-user", "empty-password-user", "invalid-password-hash-user", "undefined-groups-user"
|
|
password, otherPassword, colonPassword, noGroupPassword, oneGroupPassword, undefinedGroupsPassword :=
|
|
"some-password", "other-password", "some-:-password", "no-group-password", "one-group-password", "undefined-groups-password"
|
|
|
|
group0, group1 := "some-group-0", "some-group-1"
|
|
groups := group0 + " , " + group1
|
|
|
|
kubeClient := kubernetesfake.NewSimpleClientset()
|
|
addSecretToFakeClientTracker(t, kubeClient, user, password, groups)
|
|
addSecretToFakeClientTracker(t, kubeClient, otherUser, otherPassword, groups)
|
|
addSecretToFakeClientTracker(t, kubeClient, colonUser, colonPassword, groups)
|
|
addSecretToFakeClientTracker(t, kubeClient, noGroupUser, noGroupPassword, "")
|
|
addSecretToFakeClientTracker(t, kubeClient, oneGroupUser, oneGroupPassword, group0)
|
|
addSecretToFakeClientTracker(t, kubeClient, emptyPasswordUser, "", groups)
|
|
|
|
require.NoError(t, kubeClient.Tracker().Add(&corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: passwordUndefinedUser,
|
|
Namespace: namespace,
|
|
},
|
|
Data: map[string][]byte{
|
|
"groups": []byte(groups),
|
|
},
|
|
}))
|
|
|
|
undefinedGroupsUserPasswordHash, err := bcrypt.GenerateFromPassword([]byte(undefinedGroupsPassword), bcrypt.MinCost)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, kubeClient.Tracker().Add(&corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: undefinedGroupsUser,
|
|
Namespace: namespace,
|
|
},
|
|
Data: map[string][]byte{
|
|
"passwordHash": undefinedGroupsUserPasswordHash,
|
|
},
|
|
}))
|
|
|
|
require.NoError(t, kubeClient.Tracker().Add(&corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: invalidPasswordHashUser,
|
|
Namespace: namespace,
|
|
},
|
|
Data: map[string][]byte{
|
|
"groups": []byte(groups),
|
|
"passwordHash": []byte("not a valid password hash"),
|
|
},
|
|
}))
|
|
|
|
secretInformer := createSecretInformer(ctx, t, kubeClient)
|
|
|
|
certProvider, caBundle, serverName := newCertProvider(t)
|
|
w := newWebhook(certProvider, secretInformer)
|
|
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
defer func() { _ = l.Close() }()
|
|
require.NoError(t, w.start(ctx, l))
|
|
|
|
client := newClient(t, caBundle, serverName)
|
|
|
|
goodURL := fmt.Sprintf("https://%s/authenticate", l.Addr().String())
|
|
goodRequestHeaders := map[string][]string{
|
|
"Content-Type": {"application/json; charset=UTF-8"},
|
|
"Accept": {"application/json, */*"},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
method string
|
|
headers map[string][]string
|
|
body func() (io.ReadCloser, error)
|
|
|
|
wantStatus int
|
|
wantHeaders map[string][]string
|
|
wantBody *authenticationv1beta1.TokenReview
|
|
}{
|
|
{
|
|
name: "success for a user who belongs to multiple groups",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user + ":" + password) },
|
|
wantStatus: http.StatusOK,
|
|
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
|
wantBody: authenticatedResponseJSON(user, []string{group0, group1}),
|
|
},
|
|
{
|
|
name: "success for a user who belongs to one groups",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody(oneGroupUser + ":" + oneGroupPassword) },
|
|
wantStatus: http.StatusOK,
|
|
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
|
wantBody: authenticatedResponseJSON(oneGroupUser, []string{group0}),
|
|
},
|
|
{
|
|
name: "success for a user who belongs to no groups",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody(noGroupUser + ":" + noGroupPassword) },
|
|
wantStatus: http.StatusOK,
|
|
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
|
wantBody: authenticatedResponseJSON(noGroupUser, nil),
|
|
},
|
|
{
|
|
name: "wrong username for password",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody(otherUser + ":" + password) },
|
|
wantStatus: http.StatusOK,
|
|
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
|
wantBody: unauthenticatedResponseJSON(),
|
|
},
|
|
{
|
|
name: "when a user has no password hash in the secret",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody(passwordUndefinedUser + ":foo") },
|
|
wantStatus: http.StatusOK,
|
|
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
|
wantBody: unauthenticatedResponseJSON(),
|
|
},
|
|
{
|
|
name: "when a user has an invalid password hash in the secret",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody(invalidPasswordHashUser + ":foo") },
|
|
wantStatus: http.StatusOK,
|
|
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
|
wantBody: unauthenticatedResponseJSON(),
|
|
},
|
|
{
|
|
name: "success for a user has no groups defined in the secret",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) {
|
|
return newTokenReviewBody(undefinedGroupsUser + ":" + undefinedGroupsPassword)
|
|
},
|
|
wantStatus: http.StatusOK,
|
|
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
|
wantBody: authenticatedResponseJSON(undefinedGroupsUser, nil),
|
|
},
|
|
{
|
|
name: "when a user has empty string as their password",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody(passwordUndefinedUser + ":foo") },
|
|
wantStatus: http.StatusOK,
|
|
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
|
wantBody: unauthenticatedResponseJSON(),
|
|
},
|
|
{
|
|
name: "wrong password for username",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user + ":" + otherPassword) },
|
|
wantStatus: http.StatusOK,
|
|
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
|
wantBody: unauthenticatedResponseJSON(),
|
|
},
|
|
{
|
|
name: "non-existent password for username",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user + ":" + "some-non-existent-password") },
|
|
wantStatus: http.StatusOK,
|
|
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
|
wantBody: unauthenticatedResponseJSON(),
|
|
},
|
|
{
|
|
name: "non-existent username",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-non-existent-user" + ":" + password) },
|
|
wantStatus: http.StatusOK,
|
|
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
|
wantBody: unauthenticatedResponseJSON(),
|
|
},
|
|
{
|
|
name: "bad token format (missing colon)",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user) },
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "password contains colon",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody(colonUser + ":" + colonPassword) },
|
|
wantStatus: http.StatusOK,
|
|
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
|
wantBody: authenticatedResponseJSON(colonUser, []string{group0, group1}),
|
|
},
|
|
{
|
|
name: "bad TokenReview group",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) {
|
|
return newTokenReviewBodyWithGVK(
|
|
user+":"+password,
|
|
&schema.GroupVersionKind{
|
|
Group: "bad group",
|
|
Version: authenticationv1beta1.SchemeGroupVersion.Version,
|
|
Kind: "TokenReview",
|
|
},
|
|
)
|
|
},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "bad TokenReview version",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) {
|
|
return newTokenReviewBodyWithGVK(
|
|
user+":"+password,
|
|
&schema.GroupVersionKind{
|
|
Group: authenticationv1beta1.SchemeGroupVersion.Group,
|
|
Version: "bad version",
|
|
Kind: "TokenReview",
|
|
},
|
|
)
|
|
},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "bad TokenReview kind",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) {
|
|
return newTokenReviewBodyWithGVK(
|
|
user+":"+password,
|
|
&schema.GroupVersionKind{
|
|
Group: authenticationv1beta1.SchemeGroupVersion.Group,
|
|
Version: authenticationv1beta1.SchemeGroupVersion.Version,
|
|
Kind: "wrong-kind",
|
|
},
|
|
)
|
|
},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "bad path",
|
|
url: fmt.Sprintf("https://%s/tuna", l.Addr().String()),
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-token") },
|
|
wantStatus: http.StatusNotFound,
|
|
},
|
|
{
|
|
name: "bad method",
|
|
url: goodURL,
|
|
method: http.MethodGet,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-token") },
|
|
wantStatus: http.StatusMethodNotAllowed,
|
|
},
|
|
{
|
|
name: "bad content type",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: map[string][]string{
|
|
"Content-Type": {"application/xml"},
|
|
"Accept": {"application/json"},
|
|
},
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-token") },
|
|
wantStatus: http.StatusUnsupportedMediaType,
|
|
},
|
|
{
|
|
name: "bad accept",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: map[string][]string{
|
|
"Content-Type": {"application/json"},
|
|
"Accept": {"application/xml"},
|
|
},
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-token") },
|
|
wantStatus: http.StatusUnsupportedMediaType,
|
|
},
|
|
{
|
|
name: "success when there are multiple accepts and one of them is json",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: map[string][]string{
|
|
"Content-Type": {"application/json"},
|
|
"Accept": {"something/else, application/xml, application/json"},
|
|
},
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user + ":" + password) },
|
|
wantStatus: http.StatusOK,
|
|
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
|
wantBody: authenticatedResponseJSON(user, []string{group0, group1}),
|
|
},
|
|
{
|
|
name: "success when there are multiple accepts and one of them is */*",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: map[string][]string{
|
|
"Content-Type": {"application/json"},
|
|
"Accept": {"something/else, */*, application/foo"},
|
|
},
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user + ":" + password) },
|
|
wantStatus: http.StatusOK,
|
|
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
|
wantBody: authenticatedResponseJSON(user, []string{group0, group1}),
|
|
},
|
|
{
|
|
name: "success when there are multiple accepts and one of them is application/*",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: map[string][]string{
|
|
"Content-Type": {"application/json"},
|
|
"Accept": {"something/else, application/*, application/foo"},
|
|
},
|
|
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user + ":" + password) },
|
|
wantStatus: http.StatusOK,
|
|
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
|
wantBody: authenticatedResponseJSON(user, []string{group0, group1}),
|
|
},
|
|
{
|
|
name: "bad body",
|
|
url: goodURL,
|
|
method: http.MethodPost,
|
|
headers: goodRequestHeaders,
|
|
body: func() (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewBuffer([]byte("invalid body"))), nil },
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
parsedURL, err := url.Parse(test.url)
|
|
require.NoError(t, err)
|
|
|
|
body, err := test.body()
|
|
require.NoError(t, err)
|
|
|
|
rsp, err := client.Do(&http.Request{
|
|
Method: test.method,
|
|
URL: parsedURL,
|
|
Header: test.headers,
|
|
Body: body,
|
|
})
|
|
require.NoError(t, err)
|
|
defer func() { _ = rsp.Body.Close() }()
|
|
|
|
require.Equal(t, test.wantStatus, rsp.StatusCode)
|
|
|
|
if test.wantHeaders != nil {
|
|
for k, v := range test.wantHeaders {
|
|
require.Equal(t, v, rsp.Header.Values(k))
|
|
}
|
|
}
|
|
|
|
responseBody, err := ioutil.ReadAll(rsp.Body)
|
|
require.NoError(t, err)
|
|
if test.wantBody != nil {
|
|
require.NoError(t, err)
|
|
|
|
var tr authenticationv1beta1.TokenReview
|
|
require.NoError(t, json.Unmarshal(responseBody, &tr))
|
|
require.Equal(t, test.wantBody, &tr)
|
|
} else {
|
|
require.Empty(t, responseBody)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func createSecretInformer(ctx context.Context, t *testing.T, kubeClient kubernetes.Interface) corev1informers.SecretInformer {
|
|
t.Helper()
|
|
|
|
kubeInformers := kubeinformers.NewSharedInformerFactory(kubeClient, 0)
|
|
|
|
secretInformer := kubeInformers.Core().V1().Secrets()
|
|
|
|
// We need to call Informer() on the secretInformer to lazily instantiate the
|
|
// informer factory before syncing it.
|
|
secretInformer.Informer()
|
|
|
|
kubeInformers.Start(ctx.Done())
|
|
|
|
informerTypesSynced := kubeInformers.WaitForCacheSync(ctx.Done())
|
|
require.True(t, informerTypesSynced[reflect.TypeOf(&corev1.Secret{})])
|
|
|
|
return secretInformer
|
|
}
|
|
|
|
// newClientProvider returns a dynamiccert.Provider configured
|
|
// with valid serving cert, the CA bundle that can be used to verify the serving
|
|
// cert, and the server name that can be used to verify the TLS peer.
|
|
func newCertProvider(t *testing.T) (dynamiccert.Private, []byte, string) {
|
|
t.Helper()
|
|
|
|
serverName := "local-user-authenticator"
|
|
|
|
ca, err := certauthority.New(serverName+" CA", time.Hour*24)
|
|
require.NoError(t, err)
|
|
|
|
cert, err := ca.IssueServerCert([]string{serverName}, nil, time.Hour*24)
|
|
require.NoError(t, err)
|
|
|
|
certPEM, keyPEM, err := certauthority.ToPEM(cert)
|
|
require.NoError(t, err)
|
|
|
|
certProvider := dynamiccert.NewServingCert(t.Name())
|
|
err = certProvider.SetCertKeyContent(certPEM, keyPEM)
|
|
require.NoError(t, err)
|
|
|
|
return certProvider, ca.Bundle(), serverName
|
|
}
|
|
|
|
// newClient creates an http.Client that can be used to make an HTTPS call to a
|
|
// service whose serving certs can be verified by the provided CA bundle.
|
|
func newClient(t *testing.T, caBundle []byte, serverName string) *http.Client {
|
|
t.Helper()
|
|
|
|
rootCAs := x509.NewCertPool()
|
|
ok := rootCAs.AppendCertsFromPEM(caBundle)
|
|
require.True(t, ok)
|
|
|
|
c := phttp.Secure(rootCAs)
|
|
|
|
tlsConfig, err := utilnet.TLSClientConfig(c.Transport)
|
|
require.NoError(t, err)
|
|
tlsConfig.ServerName = serverName
|
|
|
|
return c
|
|
}
|
|
|
|
// newTokenReviewBody creates an io.ReadCloser that contains a JSON-encodeed
|
|
// TokenReview request with expected APIVersion and Kind fields.
|
|
func newTokenReviewBody(token string) (io.ReadCloser, error) {
|
|
return newTokenReviewBodyWithGVK(
|
|
token,
|
|
&schema.GroupVersionKind{
|
|
Group: authenticationv1beta1.SchemeGroupVersion.Group,
|
|
Version: authenticationv1beta1.SchemeGroupVersion.Version,
|
|
Kind: "TokenReview",
|
|
},
|
|
)
|
|
}
|
|
|
|
// newTokenReviewBodyWithGVK creates an io.ReadCloser that contains a
|
|
// JSON-encoded TokenReview request. The TypeMeta fields of the TokenReview are
|
|
// filled in with the provided gvk.
|
|
func newTokenReviewBodyWithGVK(token string, gvk *schema.GroupVersionKind) (io.ReadCloser, error) {
|
|
buf := bytes.NewBuffer([]byte{})
|
|
tr := authenticationv1beta1.TokenReview{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: gvk.GroupVersion().String(),
|
|
Kind: gvk.Kind,
|
|
},
|
|
Spec: authenticationv1beta1.TokenReviewSpec{
|
|
Token: token,
|
|
},
|
|
}
|
|
err := json.NewEncoder(buf).Encode(&tr)
|
|
return ioutil.NopCloser(buf), err
|
|
}
|
|
|
|
func unauthenticatedResponseJSON() *authenticationv1beta1.TokenReview {
|
|
return &authenticationv1beta1.TokenReview{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "TokenReview",
|
|
APIVersion: "authentication.k8s.io/v1beta1",
|
|
},
|
|
Status: authenticationv1beta1.TokenReviewStatus{
|
|
Authenticated: false,
|
|
},
|
|
}
|
|
}
|
|
|
|
func authenticatedResponseJSON(user string, groups []string) *authenticationv1beta1.TokenReview {
|
|
return &authenticationv1beta1.TokenReview{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "TokenReview",
|
|
APIVersion: "authentication.k8s.io/v1beta1",
|
|
},
|
|
Status: authenticationv1beta1.TokenReviewStatus{
|
|
Authenticated: true,
|
|
User: authenticationv1beta1.UserInfo{
|
|
Username: user,
|
|
Groups: groups,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func addSecretToFakeClientTracker(t *testing.T, kubeClient *kubernetesfake.Clientset, username, password, groups string) {
|
|
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
|
|
require.NoError(t, err)
|
|
|
|
secret := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: username,
|
|
Namespace: namespace,
|
|
},
|
|
Data: map[string][]byte{
|
|
"passwordHash": passwordHash,
|
|
"groups": []byte(groups),
|
|
},
|
|
}
|
|
|
|
require.NoError(t, kubeClient.Tracker().Add(secret))
|
|
}
|