cmd/local-user-authenticator: go back to use TokenReview structs

So I looked into other TokenReview webhook implementations, and most
of them just use the json stdlib package to unmarshal/marshal
TokenReview payloads. I'd say let's follow that pattern, even though
it leads to extra fields in the JSON payload (these are not harmful).

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
This commit is contained in:
Andrew Keesler 2020-09-11 15:19:05 -04:00
parent 17d40b7a73
commit 19c671a60a
No known key found for this signature in database
GPG Key ID: 27CE0444346F9413
2 changed files with 98 additions and 66 deletions

View File

@ -29,6 +29,7 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
authenticationv1beta1 "k8s.io/api/authentication/v1beta1" authenticationv1beta1 "k8s.io/api/authentication/v1beta1"
k8serrors "k8s.io/apimachinery/pkg/api/errors" k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubeinformers "k8s.io/client-go/informers" kubeinformers "k8s.io/client-go/informers"
corev1informers "k8s.io/client-go/informers/core/v1" corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
@ -47,9 +48,6 @@ const (
// This string must match the name of the Service declared in the deployment yaml. // This string must match the name of the Service declared in the deployment yaml.
serviceName = "local-user-authenticator" serviceName = "local-user-authenticator"
unauthenticatedResponse = `{"apiVersion":"authentication.k8s.io/v1beta1","kind":"TokenReview","status":{"authenticated":false}}`
authenticatedResponseTemplate = `{"apiVersion":"authentication.k8s.io/v1beta1","kind":"TokenReview","status":{"authenticated":true,"user":{"username":"%s","uid":"%s","groups":%s}}}`
singletonWorker = 1 singletonWorker = 1
defaultResyncInterval = 3 * time.Minute defaultResyncInterval = 3 * time.Minute
@ -240,7 +238,20 @@ func trimLeadingAndTrailingWhitespace(ss []string) {
func respondWithUnauthenticated(rsp http.ResponseWriter) { func respondWithUnauthenticated(rsp http.ResponseWriter) {
rsp.Header().Add("Content-Type", "application/json") rsp.Header().Add("Content-Type", "application/json")
_, _ = rsp.Write([]byte(unauthenticatedResponse))
body := authenticationv1beta1.TokenReview{
TypeMeta: metav1.TypeMeta{
Kind: "TokenReview",
APIVersion: authenticationv1beta1.SchemeGroupVersion.String(),
},
Status: authenticationv1beta1.TokenReviewStatus{
Authenticated: false,
},
}
if err := json.NewEncoder(rsp).Encode(body); err != nil {
klog.InfoS("could not encode response", "err", err)
rsp.WriteHeader(http.StatusInternalServerError)
}
} }
func respondWithAuthenticated( func respondWithAuthenticated(
@ -249,14 +260,24 @@ func respondWithAuthenticated(
groups []string, groups []string,
) { ) {
rsp.Header().Add("Content-Type", "application/json") rsp.Header().Add("Content-Type", "application/json")
groupsJSONBytes, err := json.Marshal(groups) body := authenticationv1beta1.TokenReview{
if err != nil { TypeMeta: metav1.TypeMeta{
Kind: "TokenReview",
APIVersion: authenticationv1beta1.SchemeGroupVersion.String(),
},
Status: authenticationv1beta1.TokenReviewStatus{
Authenticated: true,
User: authenticationv1beta1.UserInfo{
Username: username,
Groups: groups,
UID: uid,
},
},
}
if err := json.NewEncoder(rsp).Encode(body); err != nil {
klog.InfoS("could not encode response", "err", err) klog.InfoS("could not encode response", "err", err)
rsp.WriteHeader(http.StatusInternalServerError) rsp.WriteHeader(http.StatusInternalServerError)
return
} }
jsonBody := fmt.Sprintf(authenticatedResponseTemplate, username, uid, groupsJSONBytes)
_, _ = rsp.Write([]byte(jsonBody))
} }
func newK8sClient() (kubernetes.Interface, error) { func newK8sClient() (kubernetes.Interface, error) {

View File

@ -19,7 +19,6 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"reflect" "reflect"
"strings"
"testing" "testing"
"time" "time"
@ -28,6 +27,7 @@ import (
authenticationv1beta1 "k8s.io/api/authentication/v1beta1" authenticationv1beta1 "k8s.io/api/authentication/v1beta1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
kubeinformers "k8s.io/client-go/informers" kubeinformers "k8s.io/client-go/informers"
corev1informers "k8s.io/client-go/informers/core/v1" corev1informers "k8s.io/client-go/informers/core/v1"
@ -128,7 +128,7 @@ func TestWebhook(t *testing.T) {
wantStatus int wantStatus int
wantHeaders map[string][]string wantHeaders map[string][]string
wantBody *string wantBody *authenticationv1beta1.TokenReview
}{ }{
{ {
name: "success for a user who belongs to multiple groups", name: "success for a user who belongs to multiple groups",
@ -158,7 +158,7 @@ func TestWebhook(t *testing.T) {
body: func() (io.ReadCloser, error) { return newTokenReviewBody(noGroupUser + ":" + noGroupPassword) }, body: func() (io.ReadCloser, error) { return newTokenReviewBody(noGroupUser + ":" + noGroupPassword) },
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantHeaders: map[string][]string{"Content-Type": {"application/json"}}, wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
wantBody: authenticatedResponseJSON(noGroupUser, noGroupUID, []string{}), wantBody: authenticatedResponseJSON(noGroupUser, noGroupUID, nil),
}, },
{ {
name: "wrong username for password", name: "wrong username for password",
@ -200,7 +200,7 @@ func TestWebhook(t *testing.T) {
}, },
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantHeaders: map[string][]string{"Content-Type": {"application/json"}}, wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
wantBody: authenticatedResponseJSON(undefinedGroupsUser, undefinedGroupsUID, []string{}), wantBody: authenticatedResponseJSON(undefinedGroupsUser, undefinedGroupsUID, nil),
}, },
{ {
name: "when a user has empty string as their password", name: "when a user has empty string as their password",
@ -266,9 +266,13 @@ func TestWebhook(t *testing.T) {
method: http.MethodPost, method: http.MethodPost,
headers: goodRequestHeaders, headers: goodRequestHeaders,
body: func() (io.ReadCloser, error) { body: func() (io.ReadCloser, error) {
return newTokenReviewBody( return newTokenReviewBodyWithGVK(
user+":"+password, user+":"+password,
"wrong-group/v1", &schema.GroupVersionKind{
Group: "bad group",
Version: authenticationv1beta1.SchemeGroupVersion.Version,
Kind: "TokenReview",
},
) )
}, },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
@ -279,9 +283,13 @@ func TestWebhook(t *testing.T) {
method: http.MethodPost, method: http.MethodPost,
headers: goodRequestHeaders, headers: goodRequestHeaders,
body: func() (io.ReadCloser, error) { body: func() (io.ReadCloser, error) {
return newTokenReviewBody( return newTokenReviewBodyWithGVK(
user+":"+password, user+":"+password,
"authentication.k8s.io/wrong-version", &schema.GroupVersionKind{
Group: authenticationv1beta1.SchemeGroupVersion.Group,
Version: "bad version",
Kind: "TokenReview",
},
) )
}, },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
@ -292,10 +300,13 @@ func TestWebhook(t *testing.T) {
method: http.MethodPost, method: http.MethodPost,
headers: goodRequestHeaders, headers: goodRequestHeaders,
body: func() (io.ReadCloser, error) { body: func() (io.ReadCloser, error) {
return newTokenReviewBody( return newTokenReviewBodyWithGVK(
user+":"+password, user+":"+password,
authenticationv1beta1.SchemeGroupVersion.String(), &schema.GroupVersionKind{
"wrong-kind", Group: authenticationv1beta1.SchemeGroupVersion.Group,
Version: authenticationv1beta1.SchemeGroupVersion.Version,
Kind: "wrong-kind",
},
) )
}, },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
@ -417,7 +428,10 @@ func TestWebhook(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
if test.wantBody != nil { if test.wantBody != nil {
require.NoError(t, err) require.NoError(t, err)
require.JSONEq(t, *test.wantBody, string(responseBody))
var tr authenticationv1beta1.TokenReview
require.NoError(t, json.Unmarshal(responseBody, &tr))
require.Equal(t, test.wantBody, &tr)
} else { } else {
require.Empty(t, responseBody) require.Empty(t, responseBody)
} }
@ -486,24 +500,28 @@ func newClient(caBundle []byte, serverName string) *http.Client {
} }
} }
// newTokenReviewBody creates an io.ReadCloser that contains a JSON-encoded // newTokenReviewBody creates an io.ReadCloser that contains a JSON-encodeed
// TokenReview request. // TokenReview request with expected APIVersion and Kind fields.
func newTokenReviewBody(token string, extra ...string) (io.ReadCloser, error) { func newTokenReviewBody(token string) (io.ReadCloser, error) {
v := authenticationv1beta1.SchemeGroupVersion.String() return newTokenReviewBodyWithGVK(
if len(extra) > 0 { token,
v = extra[0] &schema.GroupVersionKind{
} Group: authenticationv1beta1.SchemeGroupVersion.Group,
Version: authenticationv1beta1.SchemeGroupVersion.Version,
k := "TokenReview" Kind: "TokenReview",
if len(extra) > 1 { },
k = extra[1] )
} }
// 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{}) buf := bytes.NewBuffer([]byte{})
tr := authenticationv1beta1.TokenReview{ tr := authenticationv1beta1.TokenReview{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
APIVersion: v, APIVersion: gvk.GroupVersion().String(),
Kind: k, Kind: gvk.Kind,
}, },
Spec: authenticationv1beta1.TokenReviewSpec{ Spec: authenticationv1beta1.TokenReviewSpec{
Token: token, Token: token,
@ -513,40 +531,33 @@ func newTokenReviewBody(token string, extra ...string) (io.ReadCloser, error) {
return ioutil.NopCloser(buf), err return ioutil.NopCloser(buf), err
} }
func unauthenticatedResponseJSON() *string { func unauthenticatedResponseJSON() *authenticationv1beta1.TokenReview {
// Very specific expected result. Avoid using json package so we know exactly what we're asserting. return &authenticationv1beta1.TokenReview{
s := `{ TypeMeta: metav1.TypeMeta{
"apiVersion": "authentication.k8s.io/v1beta1", Kind: "TokenReview",
"kind": "TokenReview", APIVersion: "authentication.k8s.io/v1beta1",
"status": { },
"authenticated": false Status: authenticationv1beta1.TokenReviewStatus{
} Authenticated: false,
}` },
return &s }
} }
func authenticatedResponseJSON(user, uid string, groups []string) *string { func authenticatedResponseJSON(user, uid string, groups []string) *authenticationv1beta1.TokenReview {
quotedGroups := make([]string, len(groups)) return &authenticationv1beta1.TokenReview{
for i, group := range groups { TypeMeta: metav1.TypeMeta{
quotedGroups[i] = `"` + group + `"` Kind: "TokenReview",
APIVersion: "authentication.k8s.io/v1beta1",
},
Status: authenticationv1beta1.TokenReviewStatus{
Authenticated: true,
User: authenticationv1beta1.UserInfo{
Username: user,
Groups: groups,
UID: uid,
},
},
} }
// Very specific expected result. Avoid using json package so we know exactly what we're asserting.
authenticatedJSONTemplate := `{
"apiVersion": "authentication.k8s.io/v1beta1",
"kind": "TokenReview",
"status": {
"authenticated": true,
"user": {
"username": "%s",
"uid": "%s",
"groups": [%s]
}
}
}`
s := fmt.Sprintf(authenticatedJSONTemplate, user, uid, strings.Join(quotedGroups, ","))
return &s
} }
func addSecretToFakeClientTracker(t *testing.T, kubeClient *kubernetesfake.Clientset, username, uid, password, groups string) { func addSecretToFakeClientTracker(t *testing.T, kubeClient *kubernetesfake.Clientset, username, uid, password, groups string) {