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/csv"
"encoding/json" "encoding/json"
"fmt" "fmt"
"mime"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -122,13 +123,15 @@ func (w *webhook) ServeHTTP(rsp http.ResponseWriter, req *http.Request) {
return return
} }
if !contains(req.Header.Values("Content-Type"), "application/json") { if !headerContains(req, "Content-Type", "application/json") {
klog.InfoS("wrong content type", "Content-Type", req.Header.Values("Content-Type")) klog.InfoS("content type is not application/json", "Content-Type", req.Header.Values("Content-Type"))
rsp.WriteHeader(http.StatusUnsupportedMediaType) rsp.WriteHeader(http.StatusUnsupportedMediaType)
return return
} }
if !contains(req.Header.Values("Accept"), "application/json") { if !headerContains(req, "Accept", "application/json") &&
klog.InfoS("wrong accept type", "Accept", req.Header.Values("Accept")) !headerContains(req, "Accept", "application/*") &&
!headerContains(req, "Accept", "*/*") {
klog.InfoS("client does not accept application/json", "Accept", req.Header.Values("Accept"))
rsp.WriteHeader(http.StatusUnsupportedMediaType) rsp.WriteHeader(http.StatusUnsupportedMediaType)
return return
} }
@ -190,10 +193,15 @@ func (w *webhook) ServeHTTP(rsp http.ResponseWriter, req *http.Request) {
respondWithAuthenticated(rsp, secret.ObjectMeta.Name, string(secret.UID), groups) respondWithAuthenticated(rsp, secret.ObjectMeta.Name, string(secret.UID), groups)
} }
func contains(ss []string, s string) bool { func headerContains(req *http.Request, headerName, s string) bool {
for i := range ss { headerValues := req.Header.Values(headerName)
if ss[i] == s { for i := range headerValues {
return true mimeTypes := strings.Split(headerValues[i], ",")
for _, mimeType := range mimeTypes {
mediaType, _, _ := mime.ParseMediaType(mimeType)
if mediaType == s {
return true
}
} }
} }
return false return false

View File

@ -101,8 +101,8 @@ func TestWebhook(t *testing.T) {
goodURL := fmt.Sprintf("https://%s/authenticate", l.Addr().String()) goodURL := fmt.Sprintf("https://%s/authenticate", l.Addr().String())
goodRequestHeaders := map[string][]string{ goodRequestHeaders := map[string][]string{
"Content-Type": {"application/json"}, "Content-Type": {"application/json; charset=UTF-8"},
"Accept": {"application/json"}, "Accept": {"application/json, */*"},
} }
tests := []struct { tests := []struct {
@ -117,74 +117,54 @@ func TestWebhook(t *testing.T) {
wantBody *string wantBody *string
}{ }{
{ {
name: "success for a user who belongs to multiple groups", name: "success for a user who belongs to multiple groups",
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(user + ":" + password) },
return newTokenReviewBody(user + ":" + password) wantStatus: http.StatusOK,
}, wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
wantStatus: http.StatusOK, wantBody: authenticatedResponseJSON(user, uid, []string{group0, group1}),
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", name: "success for a user who belongs to one groups",
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(oneGroupUser + ":" + oneGroupPassword) },
return newTokenReviewBody(oneGroupUser + ":" + oneGroupPassword) wantStatus: http.StatusOK,
}, wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
wantStatus: http.StatusOK, wantBody: authenticatedResponseJSON(oneGroupUser, oneGroupUID, []string{group0}),
wantHeaders: map[string][]string{
"Content-Type": {"application/json"},
},
wantBody: authenticatedResponseJSON(oneGroupUser, oneGroupUID, []string{group0}),
}, },
{ {
name: "success for a user who belongs to no groups", name: "success for a user who belongs to no groups",
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(noGroupUser + ":" + noGroupPassword) },
return newTokenReviewBody(noGroupUser + ":" + noGroupPassword) wantStatus: http.StatusOK,
}, wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
wantStatus: http.StatusOK, wantBody: authenticatedResponseJSON(noGroupUser, noGroupUID, []string{}),
wantHeaders: map[string][]string{
"Content-Type": {"application/json"},
},
wantBody: authenticatedResponseJSON(noGroupUser, noGroupUID, []string{}),
}, },
{ {
name: "wrong username for password", name: "wrong username for password",
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(otherUser + ":" + password) },
return newTokenReviewBody(otherUser + ":" + password) wantStatus: http.StatusOK,
}, wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
wantStatus: http.StatusOK, wantBody: unauthenticatedResponseJSON(),
wantHeaders: map[string][]string{
"Content-Type": {"application/json"},
},
wantBody: unauthenticatedResponseJSON(),
}, },
{ {
name: "when a user has no password hash in the secret", name: "when a user has no password hash 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(passwordUndefinedUser + ":foo") },
return newTokenReviewBody(passwordUndefinedUser + ":foo") wantStatus: http.StatusOK,
}, wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
wantStatus: http.StatusOK, wantBody: unauthenticatedResponseJSON(),
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",
@ -194,110 +174,82 @@ func TestWebhook(t *testing.T) {
body: func() (io.ReadCloser, error) { body: func() (io.ReadCloser, error) {
return newTokenReviewBody(underfinedGroupsUser + ":" + undefinedGroupsPassword) return newTokenReviewBody(underfinedGroupsUser + ":" + undefinedGroupsPassword)
}, },
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantHeaders: map[string][]string{ wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
"Content-Type": {"application/json"}, wantBody: authenticatedResponseJSON(underfinedGroupsUser, underfinedGroupsUID, []string{}),
},
wantBody: authenticatedResponseJSON(underfinedGroupsUser, underfinedGroupsUID, []string{}),
}, },
{ {
name: "when a user has empty string as their password", name: "when a user has empty string as their password",
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(passwordUndefinedUser + ":foo") },
return newTokenReviewBody(passwordUndefinedUser + ":foo") wantStatus: http.StatusOK,
}, wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
wantStatus: http.StatusOK, wantBody: unauthenticatedResponseJSON(),
wantHeaders: map[string][]string{
"Content-Type": {"application/json"},
},
wantBody: unauthenticatedResponseJSON(),
}, },
{ {
name: "wrong password for username", name: "wrong password for username",
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(user + ":" + otherPassword) },
return newTokenReviewBody(user + ":" + otherPassword) wantStatus: http.StatusOK,
}, wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
wantStatus: http.StatusOK, wantBody: unauthenticatedResponseJSON(),
wantHeaders: map[string][]string{
"Content-Type": {"application/json"},
},
wantBody: unauthenticatedResponseJSON(),
}, },
{ {
name: "non-existent password for username", name: "non-existent password for username",
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(user + ":" + "some-non-existent-password") },
return newTokenReviewBody(user + ":" + "some-non-existent-password") wantStatus: http.StatusOK,
}, wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
wantStatus: http.StatusOK, wantBody: unauthenticatedResponseJSON(),
wantHeaders: map[string][]string{
"Content-Type": {"application/json"},
},
wantBody: unauthenticatedResponseJSON(),
}, },
{ {
name: "non-existent username", name: "non-existent username",
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("some-non-existent-user" + ":" + password) },
return newTokenReviewBody("some-non-existent-user" + ":" + password) wantStatus: http.StatusOK,
}, wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
wantStatus: http.StatusOK, wantBody: unauthenticatedResponseJSON(),
wantHeaders: map[string][]string{
"Content-Type": {"application/json"},
},
wantBody: unauthenticatedResponseJSON(),
}, },
{ {
name: "bad token format (missing colon)", name: "bad token format (missing colon)",
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(user) },
return newTokenReviewBody(user)
},
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
}, },
{ {
name: "password contains colon", name: "password contains colon",
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(colonUser + ":" + colonPassword) },
return newTokenReviewBody(colonUser + ":" + colonPassword) wantStatus: http.StatusOK,
}, wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
wantStatus: http.StatusOK, wantBody: authenticatedResponseJSON(colonUser, colonUID, []string{group0, group1}),
wantHeaders: map[string][]string{
"Content-Type": {"application/json"},
},
wantBody: authenticatedResponseJSON(colonUser, colonUID, []string{group0, group1}),
}, },
{ {
name: "bad path", name: "bad path",
url: fmt.Sprintf("https://%s/tuna", l.Addr().String()), url: fmt.Sprintf("https://%s/tuna", l.Addr().String()),
method: http.MethodPost, method: http.MethodPost,
headers: goodRequestHeaders, headers: goodRequestHeaders,
body: func() (io.ReadCloser, error) { body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-token") },
return newTokenReviewBody("some-token")
},
wantStatus: http.StatusNotFound, wantStatus: http.StatusNotFound,
}, },
{ {
name: "bad method", name: "bad method",
url: goodURL, url: goodURL,
method: http.MethodGet, method: http.MethodGet,
headers: goodRequestHeaders, headers: goodRequestHeaders,
body: func() (io.ReadCloser, error) { body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-token") },
return newTokenReviewBody("some-token")
},
wantStatus: http.StatusMethodNotAllowed, wantStatus: http.StatusMethodNotAllowed,
}, },
{ {
@ -308,9 +260,7 @@ func TestWebhook(t *testing.T) {
"Content-Type": {"application/xml"}, "Content-Type": {"application/xml"},
"Accept": {"application/json"}, "Accept": {"application/json"},
}, },
body: func() (io.ReadCloser, error) { body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-token") },
return newTokenReviewBody("some-token")
},
wantStatus: http.StatusUnsupportedMediaType, wantStatus: http.StatusUnsupportedMediaType,
}, },
{ {
@ -321,19 +271,54 @@ func TestWebhook(t *testing.T) {
"Content-Type": {"application/json"}, "Content-Type": {"application/json"},
"Accept": {"application/xml"}, "Accept": {"application/xml"},
}, },
body: func() (io.ReadCloser, error) { body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-token") },
return newTokenReviewBody("some-token")
},
wantStatus: http.StatusUnsupportedMediaType, wantStatus: http.StatusUnsupportedMediaType,
}, },
{ {
name: "bad body", name: "success when there are multiple accepts and one of them is json",
url: goodURL, url: goodURL,
method: http.MethodPost, method: http.MethodPost,
headers: goodRequestHeaders, headers: map[string][]string{
body: func() (io.ReadCloser, error) { "Content-Type": {"application/json"},
return ioutil.NopCloser(bytes.NewBuffer([]byte("invalid body"))), nil "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, wantStatus: http.StatusBadRequest,
}, },
} }