diff --git a/cmd/test-webhook/main.go b/cmd/test-webhook/main.go index 8c3622bd..9214ad0d 100644 --- a/cmd/test-webhook/main.go +++ b/cmd/test-webhook/main.go @@ -18,6 +18,7 @@ import ( "encoding/csv" "encoding/json" "fmt" + "mime" "net" "net/http" "os" @@ -122,13 +123,15 @@ func (w *webhook) ServeHTTP(rsp http.ResponseWriter, req *http.Request) { return } - if !contains(req.Header.Values("Content-Type"), "application/json") { - klog.InfoS("wrong content type", "Content-Type", req.Header.Values("Content-Type")) + if !headerContains(req, "Content-Type", "application/json") { + klog.InfoS("content type is not application/json", "Content-Type", req.Header.Values("Content-Type")) rsp.WriteHeader(http.StatusUnsupportedMediaType) return } - if !contains(req.Header.Values("Accept"), "application/json") { - klog.InfoS("wrong accept type", "Accept", req.Header.Values("Accept")) + if !headerContains(req, "Accept", "application/json") && + !headerContains(req, "Accept", "application/*") && + !headerContains(req, "Accept", "*/*") { + klog.InfoS("client does not accept application/json", "Accept", req.Header.Values("Accept")) rsp.WriteHeader(http.StatusUnsupportedMediaType) return } @@ -190,10 +193,15 @@ func (w *webhook) ServeHTTP(rsp http.ResponseWriter, req *http.Request) { respondWithAuthenticated(rsp, secret.ObjectMeta.Name, string(secret.UID), groups) } -func contains(ss []string, s string) bool { - for i := range ss { - if ss[i] == s { - return true +func headerContains(req *http.Request, headerName, s string) bool { + headerValues := req.Header.Values(headerName) + for i := range headerValues { + mimeTypes := strings.Split(headerValues[i], ",") + for _, mimeType := range mimeTypes { + mediaType, _, _ := mime.ParseMediaType(mimeType) + if mediaType == s { + return true + } } } return false diff --git a/cmd/test-webhook/main_test.go b/cmd/test-webhook/main_test.go index 699bc24d..66de0aa0 100644 --- a/cmd/test-webhook/main_test.go +++ b/cmd/test-webhook/main_test.go @@ -101,8 +101,8 @@ func TestWebhook(t *testing.T) { goodURL := fmt.Sprintf("https://%s/authenticate", l.Addr().String()) goodRequestHeaders := map[string][]string{ - "Content-Type": {"application/json"}, - "Accept": {"application/json"}, + "Content-Type": {"application/json; charset=UTF-8"}, + "Accept": {"application/json, */*"}, } tests := []struct { @@ -117,74 +117,54 @@ func TestWebhook(t *testing.T) { wantBody *string }{ { - 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, uid, []string{group0, group1}), + 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, uid, []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, oneGroupUID, []string{group0}), + 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, oneGroupUID, []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, noGroupUID, []string{}), + 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, noGroupUID, []string{}), }, { - 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: "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 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: "success for a user has no groups defined in the secret", @@ -194,110 +174,82 @@ func TestWebhook(t *testing.T) { body: func() (io.ReadCloser, error) { return newTokenReviewBody(underfinedGroupsUser + ":" + undefinedGroupsPassword) }, - wantStatus: http.StatusOK, - wantHeaders: map[string][]string{ - "Content-Type": {"application/json"}, - }, - wantBody: authenticatedResponseJSON(underfinedGroupsUser, underfinedGroupsUID, []string{}), + wantStatus: http.StatusOK, + wantHeaders: map[string][]string{"Content-Type": {"application/json"}}, + wantBody: authenticatedResponseJSON(underfinedGroupsUser, underfinedGroupsUID, []string{}), }, { - 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: "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: "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 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: "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) - }, + 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, colonUID, []string{group0, group1}), + 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, colonUID, []string{group0, group1}), }, { - 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") - }, + 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") - }, + name: "bad method", + url: goodURL, + method: http.MethodGet, + headers: goodRequestHeaders, + body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-token") }, wantStatus: http.StatusMethodNotAllowed, }, { @@ -308,9 +260,7 @@ func TestWebhook(t *testing.T) { "Content-Type": {"application/xml"}, "Accept": {"application/json"}, }, - body: func() (io.ReadCloser, error) { - return newTokenReviewBody("some-token") - }, + body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-token") }, wantStatus: http.StatusUnsupportedMediaType, }, { @@ -321,19 +271,54 @@ func TestWebhook(t *testing.T) { "Content-Type": {"application/json"}, "Accept": {"application/xml"}, }, - body: func() (io.ReadCloser, error) { - return newTokenReviewBody("some-token") - }, + body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-token") }, wantStatus: http.StatusUnsupportedMediaType, }, { - name: "bad body", - url: goodURL, - method: http.MethodPost, - headers: goodRequestHeaders, - body: func() (io.ReadCloser, error) { - return ioutil.NopCloser(bytes.NewBuffer([]byte("invalid body"))), nil + 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, uid, []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, uid, []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, uid, []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, }, }