Fix a nil dereference crash in rest.go

This commit is contained in:
Ryan Richard 2020-09-11 13:08:54 -07:00
parent f685cd228f
commit c436f84b3d
5 changed files with 63 additions and 32 deletions

View File

@ -135,7 +135,7 @@ func (w *webhook) ServeHTTP(rsp http.ResponseWriter, req *http.Request) {
[]byte(password), []byte(password),
) == nil ) == nil
if !passwordMatches { if !passwordMatches {
klog.InfoS("invalid password in request") klog.InfoS("authentication failed: wrong password")
respondWithUnauthenticated(rsp) respondWithUnauthenticated(rsp)
return return
} }

View File

@ -41,13 +41,15 @@ import (
func TestWebhook(t *testing.T) { func TestWebhook(t *testing.T) {
t.Parallel() t.Parallel()
const namespace = "local-user-authenticator"
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
user, otherUser, colonUser, noGroupUser, oneGroupUser, passwordUndefinedUser, emptyPasswordUser, underfinedGroupsUser := 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", "undefined-groups-user" "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"
uid, otherUID, colonUID, noGroupUID, oneGroupUID, passwordUndefinedUID, emptyPasswordUID, underfinedGroupsUID := uid, otherUID, colonUID, noGroupUID, oneGroupUID, passwordUndefinedUID, emptyPasswordUID, invalidPasswordHashUID, undefinedGroupsUID :=
"some-uid", "other-uid", "colon-uid", "no-group-uid", "one-group-uid", "password-undefined-uid", "empty-password-uid", "undefined-groups-uid" "some-uid", "other-uid", "colon-uid", "no-group-uid", "one-group-uid", "password-undefined-uid", "empty-password-uid", "invalid-password-hash-uid", "undefined-groups-uid"
password, otherPassword, colonPassword, noGroupPassword, oneGroupPassword, undefinedGroupsPassword := password, otherPassword, colonPassword, noGroupPassword, oneGroupPassword, undefinedGroupsPassword :=
"some-password", "other-password", "some-:-password", "no-group-password", "one-group-password", "undefined-groups-password" "some-password", "other-password", "some-:-password", "no-group-password", "one-group-password", "undefined-groups-password"
@ -66,7 +68,7 @@ func TestWebhook(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
UID: types.UID(passwordUndefinedUID), UID: types.UID(passwordUndefinedUID),
Name: passwordUndefinedUser, Name: passwordUndefinedUser,
Namespace: "local-user-authenticator", Namespace: namespace,
}, },
Data: map[string][]byte{ Data: map[string][]byte{
"groups": []byte(groups), "groups": []byte(groups),
@ -78,15 +80,27 @@ func TestWebhook(t *testing.T) {
require.NoError(t, kubeClient.Tracker().Add(&corev1.Secret{ require.NoError(t, kubeClient.Tracker().Add(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
UID: types.UID(underfinedGroupsUID), UID: types.UID(undefinedGroupsUID),
Name: underfinedGroupsUser, Name: undefinedGroupsUser,
Namespace: "local-user-authenticator", Namespace: namespace,
}, },
Data: map[string][]byte{ Data: map[string][]byte{
"passwordHash": undefinedGroupsUserPasswordHash, "passwordHash": undefinedGroupsUserPasswordHash,
}, },
})) }))
require.NoError(t, kubeClient.Tracker().Add(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID(invalidPasswordHashUID),
Name: invalidPasswordHashUser,
Namespace: namespace,
},
Data: map[string][]byte{
"groups": []byte(groups),
"passwordHash": []byte("not a valid password hash"),
},
}))
secretInformer := createSecretInformer(t, kubeClient) secretInformer := createSecretInformer(t, kubeClient)
certProvider, caBundle, serverName := newCertProvider(t) certProvider, caBundle, serverName := newCertProvider(t)
@ -166,17 +180,27 @@ func TestWebhook(t *testing.T) {
wantHeaders: map[string][]string{"Content-Type": {"application/json"}}, wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
wantBody: unauthenticatedResponseJSON(), 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", name: "success for a user has no groups defined in the secret",
url: goodURL, url: goodURL,
method: http.MethodPost, method: http.MethodPost,
headers: goodRequestHeaders, headers: goodRequestHeaders,
body: func() (io.ReadCloser, error) { body: func() (io.ReadCloser, error) {
return newTokenReviewBody(underfinedGroupsUser + ":" + undefinedGroupsPassword) return newTokenReviewBody(undefinedGroupsUser + ":" + undefinedGroupsPassword)
}, },
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantHeaders: map[string][]string{"Content-Type": {"application/json"}}, wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
wantBody: authenticatedResponseJSON(underfinedGroupsUser, underfinedGroupsUID, []string{}), wantBody: authenticatedResponseJSON(undefinedGroupsUser, undefinedGroupsUID, []string{}),
}, },
{ {
name: "when a user has empty string as their password", name: "when a user has empty string as their password",
@ -389,15 +413,12 @@ func createSecretInformer(t *testing.T, kubeClient kubernetes.Interface) corev1i
func newCertProvider(t *testing.T) (provider.DynamicTLSServingCertProvider, []byte, string) { func newCertProvider(t *testing.T) (provider.DynamicTLSServingCertProvider, []byte, string) {
t.Helper() t.Helper()
ca, err := certauthority.New(pkix.Name{CommonName: "local-user-authenticator CA"}, time.Hour*24) serverName := "local-user-authenticator"
ca, err := certauthority.New(pkix.Name{CommonName: serverName + " CA"}, time.Hour*24)
require.NoError(t, err) require.NoError(t, err)
serverName := "local-user-authenticator" cert, err := ca.Issue(pkix.Name{CommonName: serverName}, []string{serverName}, time.Hour*24)
cert, err := ca.Issue(
pkix.Name{CommonName: serverName},
[]string{serverName},
time.Hour*24,
)
require.NoError(t, err) require.NoError(t, err)
certPEM, keyPEM, err := certauthority.ToPEM(cert) certPEM, keyPEM, err := certauthority.ToPEM(cert)
@ -482,7 +503,7 @@ func addSecretToFakeClientTracker(t *testing.T, kubeClient *kubernetesfake.Clien
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
UID: types.UID(uid), UID: types.UID(uid),
Name: username, Name: username,
Namespace: "local-user-authenticator", Namespace: namespace,
}, },
Data: map[string][]byte{ Data: map[string][]byte{
"passwordHash": passwordHash, "passwordHash": passwordHash,

View File

@ -17,7 +17,6 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
"k8s.io/utils/trace" "k8s.io/utils/trace"
@ -84,8 +83,8 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
traceFailureWithError(t, "webhook authentication", err) traceFailureWithError(t, "webhook authentication", err)
return failureResponse(), nil return failureResponse(), nil
} }
if !authenticated || authResponse.User == nil || authResponse.User.GetName() == "" { if !authenticated || authResponse == nil || authResponse.User == nil || authResponse.User.GetName() == "" {
traceSuccess(t, authResponse.User, authenticated, false) traceSuccess(t, authResponse, authenticated, false)
return failureResponse(), nil return failureResponse(), nil
} }
@ -105,7 +104,7 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
return failureResponse(), nil return failureResponse(), nil
} }
traceSuccess(t, authResponse.User, authenticated, true) traceSuccess(t, authResponse, authenticated, true)
return &pinnipedapi.CredentialRequest{ return &pinnipedapi.CredentialRequest{
Status: pinnipedapi.CredentialRequestStatus{ Status: pinnipedapi.CredentialRequestStatus{
@ -171,10 +170,10 @@ func validateRequest(ctx context.Context, obj runtime.Object, createValidation r
return credentialRequest, nil return credentialRequest, nil
} }
func traceSuccess(t *trace.Trace, user user.Info, webhookAuthenticated bool, pinnipedAuthenticated bool) { func traceSuccess(t *trace.Trace, response *authenticator.Response, webhookAuthenticated bool, pinnipedAuthenticated bool) {
userID := "<none>" userID := "<none>"
if user != nil { if response != nil && response.User != nil {
userID = user.GetUID() userID = response.User.GetUID()
} }
t.Step("success", t.Step("success",
trace.Field{Key: "userID", Value: userID}, trace.Field{Key: "userID", Value: userID},

View File

@ -194,6 +194,19 @@ func TestCreate(t *testing.T) {
requireOneLogStatement(r, logger, `"failure" failureType:webhook authentication,msg:some webhook error`) requireOneLogStatement(r, logger, `"failure" failureType:webhook authentication,msg:some webhook error`)
}) })
it("CreateSucceedsWithAnUnauthenticatedStatusWhenWebhookReturnsNilResponseWithAuthenticatedFalse", func() {
webhook := FakeToken{
returnResponse: nil,
returnUnauthenticated: false,
}
storage := NewREST(&webhook, nil)
response, err := callCreate(context.Background(), storage, validCredentialRequest())
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
requireOneLogStatement(r, logger, `"success" userID:<none>,idpAuthenticated:true,pinnipedAuthenticated:false`)
})
it("CreateSucceedsWithAnUnauthenticatedStatusWhenWebhookDoesNotReturnAnyUserInfo", func() { it("CreateSucceedsWithAnUnauthenticatedStatusWhenWebhookDoesNotReturnAnyUserInfo", func() {
webhook := FakeToken{ webhook := FakeToken{
returnResponse: &authenticator.Response{}, returnResponse: &authenticator.Response{},

View File

@ -37,11 +37,9 @@ func TestSuccessfulCredentialRequest(t *testing.T) {
response, err := makeRequest(t, validCredentialRequestSpecWithRealToken(t)) response, err := makeRequest(t, validCredentialRequestSpecWithRealToken(t))
require.NoError(t, err) require.NoError(t, err)
// Note: If this assertion fails then your TMC token might have expired. Get a fresh one and try again.
require.Empty(t, response.Status.Message)
require.Empty(t, response.Spec)
require.NotNil(t, response.Status.Credential) require.NotNil(t, response.Status.Credential)
require.Empty(t, response.Status.Message)
require.Empty(t, response.Spec)
require.Empty(t, response.Status.Credential.Token) require.Empty(t, response.Status.Credential.Token)
require.NotEmpty(t, response.Status.Credential.ClientCertificateData) require.NotEmpty(t, response.Status.Credential.ClientCertificateData)
require.Equal(t, testUsername, getCommonName(t, response.Status.Credential.ClientCertificateData)) require.Equal(t, testUsername, getCommonName(t, response.Status.Credential.ClientCertificateData))
@ -81,7 +79,7 @@ func TestSuccessfulCredentialRequest(t *testing.T) {
}, },
}) })
// Use the client which is authenticated as the TMC user to list namespaces // Use the client which is authenticated as the test user to list namespaces
var listNamespaceResponse *v1.NamespaceList var listNamespaceResponse *v1.NamespaceList
var canListNamespaces = func() bool { var canListNamespaces = func() bool {
listNamespaceResponse, err = clientWithCertFromCredentialRequest.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) listNamespaceResponse, err = clientWithCertFromCredentialRequest.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
@ -112,7 +110,7 @@ func TestSuccessfulCredentialRequest(t *testing.T) {
}, },
}) })
// Use the client which is authenticated as the TMC group to list namespaces // Use the client which is authenticated as the test user to list namespaces
var listNamespaceResponse *v1.NamespaceList var listNamespaceResponse *v1.NamespaceList
var canListNamespaces = func() bool { var canListNamespaces = func() bool {
listNamespaceResponse, err = clientWithCertFromCredentialRequest.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) listNamespaceResponse, err = clientWithCertFromCredentialRequest.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})