Improve the parsing of headers in test-webhook

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
This commit is contained in:
Ryan Richard 2020-09-10 15:00:53 -07:00 committed by Andrew Keesler
parent 56be4a6761
commit 9baea83066
2 changed files with 161 additions and 168 deletions

View File

@ -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

View File

@ -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,
},
}