From f27b6bb93af1f9b85387c8515974946613dcb1b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20Jir=C3=A9nius?= Date: Wed, 3 Feb 2021 10:56:06 +0100 Subject: [PATCH 1/2] GH-181: Added Access-Control-Allow-Credentials header in response to HTTP requests --- server/apiHandler.go | 3 +++ server/wsConn.go | 4 +--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/server/apiHandler.go b/server/apiHandler.go index 556a60e..30367ce 100644 --- a/server/apiHandler.go +++ b/server/apiHandler.go @@ -30,6 +30,9 @@ func (s *Service) initAPIHandler() error { // setCommonHeaders sets common headers such as Access-Control-*. // It returns error if the origin header does not match any allowed origin. func (s *Service) setCommonHeaders(w http.ResponseWriter, r *http.Request) error { + if s.cfg.HeaderAuth != nil { + w.Header().Set("Access-Control-Allow-Credentials", "true") + } if s.cfg.allowOrigin[0] == "*" { w.Header().Set("Access-Control-Allow-Origin", "*") return nil diff --git a/server/wsConn.go b/server/wsConn.go index aff5d60..0314634 100644 --- a/server/wsConn.go +++ b/server/wsConn.go @@ -49,10 +49,8 @@ func (s *Service) newWSConn(ws *websocket.Conn, request *http.Request, protocol return nil } - cid := xid.New() - conn := &wsConn{ - cid: cid.String(), + cid: xid.New().String(), ws: ws, request: request, serv: s, From 08366dffde092628157aaa96724d26a17b5faa19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20Jir=C3=A9nius?= Date: Wed, 3 Feb 2021 13:23:46 +0100 Subject: [PATCH 2/2] GH-181: Added tests for headerAuth and Access-Control-Allow-Credentials header --- test/14http_get_test.go | 87 +++++++++++++++++++++++++++++++++++-- test/15http_post_test.go | 85 ++++++++++++++++++++++++++++++++++-- test/21http_options_test.go | 20 +++++++-- test/resources.go | 1 + 4 files changed, 182 insertions(+), 11 deletions(-) diff --git a/test/14http_get_test.go b/test/14http_get_test.go index e09079b..6df9eb5 100644 --- a/test/14http_get_test.go +++ b/test/14http_get_test.go @@ -257,11 +257,11 @@ func TestHTTPGet_AllowOrigin_ExpectedResponse(t *testing.T) { ExpectedMissingHeaders []string // Expected response headers not to be included ExpectedBody interface{} // Expected response body }{ - {"http://localhost", "", "*", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "*"}, []string{"Vary"}, successResponse}, - {"http://localhost", "", "http://localhost", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "http://localhost", "Vary": "Origin"}, nil, successResponse}, - {"https://resgate.io", "", "http://localhost;https://resgate.io", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "https://resgate.io", "Vary": "Origin"}, nil, successResponse}, + {"http://localhost", "", "*", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "*"}, []string{"Vary", "Access-Control-Allow-Credentials"}, successResponse}, + {"http://localhost", "", "http://localhost", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "http://localhost", "Vary": "Origin"}, []string{"Access-Control-Allow-Credentials"}, successResponse}, + {"https://resgate.io", "", "http://localhost;https://resgate.io", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "https://resgate.io", "Vary": "Origin"}, []string{"Access-Control-Allow-Credentials"}, successResponse}, // Invalid requests - {"http://example.com", "", "http://localhost;https://resgate.io", http.StatusForbidden, map[string]string{"Access-Control-Allow-Origin": "http://localhost", "Vary": "Origin"}, nil, reserr.ErrForbiddenOrigin}, + {"http://example.com", "", "http://localhost;https://resgate.io", http.StatusForbidden, map[string]string{"Access-Control-Allow-Origin": "http://localhost", "Vary": "Origin"}, []string{"Access-Control-Allow-Credentials"}, reserr.ErrForbiddenOrigin}, // No Origin header in request {"", "", "*", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "*"}, []string{"Vary"}, successResponse}, {"", "", "http://localhost", http.StatusOK, nil, []string{"Access-Control-Allow-Origin", "Vary"}, successResponse}, @@ -300,3 +300,82 @@ func TestHTTPGet_AllowOrigin_ExpectedResponse(t *testing.T) { }) } } + +func TestHTTPGet_HeaderAuth_ExpectedResponse(t *testing.T) { + model := resourceData("test.model") + token := json.RawMessage(`{"user":"foo"}`) + successResponse := json.RawMessage(model) + + tbl := []struct { + AuthResponse interface{} // Response on auth request. requestTimeout means timeout. + Token interface{} // Token to send. noToken means no token events should be sent. + ExpectedHeaders map[string]string // Expected response Headers + }{ + // Without token + {requestTimeout, noToken, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {reserr.ErrNotFound, noToken, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {[]byte(`{]`), noToken, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {nil, noToken, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + // With token + {requestTimeout, token, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {reserr.ErrNotFound, token, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {[]byte(`{]`), token, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {nil, token, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + // With nil token + {requestTimeout, nil, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {reserr.ErrNotFound, nil, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {[]byte(`{]`), nil, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {nil, nil, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + } + + for i, l := range tbl { + l := l + runNamedTest(t, fmt.Sprintf("#%d", i+1), func(s *Session) { + hreq := s.HTTPRequest("GET", "/api/test/model", nil, func(req *http.Request) { + req.Header.Set("Origin", "example.com") + }) + + req := s.GetRequest(t) + req.AssertSubject(t, "auth.vault.method") + req.AssertPathPayload(t, "header.Origin", []string{"example.com"}) + // Send token + expectedToken := l.Token + if l.Token != noToken { + cid := req.PathPayload(t, "cid").(string) + s.ConnEvent(cid, "token", struct { + Token interface{} `json:"token"` + }{l.Token}) + } else { + expectedToken = nil + } + // Respond to auth request + if l.AuthResponse == requestTimeout { + req.Timeout() + } else if err, ok := l.AuthResponse.(*reserr.Error); ok { + req.RespondError(err) + } else if raw, ok := l.AuthResponse.([]byte); ok { + req.RespondRaw(raw) + } else { + req.RespondSuccess(l.AuthResponse) + } + + // Handle model get and access request + mreqs := s.GetParallelRequests(t, 2) + mreqs. + GetRequest(t, "access.test.model"). + AssertPathPayload(t, "token", expectedToken). + RespondSuccess(json.RawMessage(`{"get":true}`)) + mreqs. + GetRequest(t, "get.test.model"). + RespondSuccess(json.RawMessage(`{"model":` + model + `}`)) + + // Validate http response + hreq.GetResponse(t). + Equals(t, http.StatusOK, successResponse). + AssertHeaders(t, l.ExpectedHeaders) + }, func(cfg *server.Config) { + headerAuth := "vault.method" + cfg.HeaderAuth = &headerAuth + }) + } +} diff --git a/test/15http_post_test.go b/test/15http_post_test.go index fd77ad5..341c496 100644 --- a/test/15http_post_test.go +++ b/test/15http_post_test.go @@ -268,11 +268,11 @@ func TestHTTPPost_AllowOrigin_ExpectedResponse(t *testing.T) { ExpectedMissingHeaders []string // Expected response headers not to be included ExpectedBody interface{} // Expected response body }{ - {"http://localhost", "", "*", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "*"}, []string{"Vary"}, successResponse}, - {"http://localhost", "", "http://localhost", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "http://localhost", "Vary": "Origin"}, nil, successResponse}, - {"https://resgate.io", "", "http://localhost;https://resgate.io", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "https://resgate.io", "Vary": "Origin"}, nil, successResponse}, + {"http://localhost", "", "*", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "*"}, []string{"Vary", "Access-Control-Allow-Credentials"}, successResponse}, + {"http://localhost", "", "http://localhost", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "http://localhost", "Vary": "Origin"}, []string{"Access-Control-Allow-Credentials"}, successResponse}, + {"https://resgate.io", "", "http://localhost;https://resgate.io", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "https://resgate.io", "Vary": "Origin"}, []string{"Access-Control-Allow-Credentials"}, successResponse}, // Invalid requests - {"http://example.com", "", "http://localhost;https://resgate.io", http.StatusForbidden, map[string]string{"Access-Control-Allow-Origin": "http://localhost", "Vary": "Origin"}, nil, reserr.ErrForbiddenOrigin}, + {"http://example.com", "", "http://localhost;https://resgate.io", http.StatusForbidden, map[string]string{"Access-Control-Allow-Origin": "http://localhost", "Vary": "Origin"}, []string{"Access-Control-Allow-Credentials"}, reserr.ErrForbiddenOrigin}, // No Origin header in request {"", "", "*", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "*"}, []string{"Vary"}, successResponse}, {"", "", "http://localhost", http.StatusOK, nil, []string{"Access-Control-Allow-Origin", "Vary"}, successResponse}, @@ -311,3 +311,80 @@ func TestHTTPPost_AllowOrigin_ExpectedResponse(t *testing.T) { }) } } + +func TestHTTPPost_HeaderAuth_ExpectedResponse(t *testing.T) { + token := json.RawMessage(`{"user":"foo"}`) + successResponse := json.RawMessage(`{"foo":"bar"}`) + + tbl := []struct { + AuthResponse interface{} // Response on auth request. requestTimeout means timeout. + Token interface{} // Token to send. noToken means no token events should be sent. + ExpectedHeaders map[string]string // Expected response Headers + }{ + // Without token + {requestTimeout, noToken, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {reserr.ErrNotFound, noToken, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {[]byte(`{]`), noToken, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {nil, noToken, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + // With token + {requestTimeout, token, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {reserr.ErrNotFound, token, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {[]byte(`{]`), token, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {nil, token, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + // With nil token + {requestTimeout, nil, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {reserr.ErrNotFound, nil, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {[]byte(`{]`), nil, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + {nil, nil, map[string]string{"Access-Control-Allow-Credentials": "true"}}, + } + + for i, l := range tbl { + l := l + runNamedTest(t, fmt.Sprintf("#%d", i+1), func(s *Session) { + hreq := s.HTTPRequest("POST", "/api/test/model/method", nil, func(req *http.Request) { + req.Header.Set("Origin", "example.com") + }) + + req := s.GetRequest(t) + req.AssertSubject(t, "auth.vault.method") + req.AssertPathPayload(t, "header.Origin", []string{"example.com"}) + // Send token + expectedToken := l.Token + if l.Token != noToken { + cid := req.PathPayload(t, "cid").(string) + s.ConnEvent(cid, "token", struct { + Token interface{} `json:"token"` + }{l.Token}) + } else { + expectedToken = nil + } + // Respond to auth request + if l.AuthResponse == requestTimeout { + req.Timeout() + } else if err, ok := l.AuthResponse.(*reserr.Error); ok { + req.RespondError(err) + } else if raw, ok := l.AuthResponse.([]byte); ok { + req.RespondRaw(raw) + } else { + req.RespondSuccess(l.AuthResponse) + } + + // Handle model get and access request + s.GetRequest(t). + AssertSubject(t, "access.test.model"). + AssertPathPayload(t, "token", expectedToken). + RespondSuccess(json.RawMessage(`{"get":true,"call":"*"}`)) + s.GetRequest(t). + AssertSubject(t, "call.test.model.method"). + RespondSuccess(successResponse) + + // Validate http response + hreq.GetResponse(t). + Equals(t, http.StatusOK, successResponse). + AssertHeaders(t, l.ExpectedHeaders) + }, func(cfg *server.Config) { + headerAuth := "vault.method" + cfg.HeaderAuth = &headerAuth + }) + } +} diff --git a/test/21http_options_test.go b/test/21http_options_test.go index 63cd9d1..01de140 100644 --- a/test/21http_options_test.go +++ b/test/21http_options_test.go @@ -49,9 +49,9 @@ func TestHTTPOptions_RequestHeaders_ExpectedResponseHeaders(t *testing.T) { ExpectedHeaders map[string]string // Expected response Headers ExpectedMissingHeaders []string // Expected response headers not to be included }{ - {[]string{"Content-Type"}, map[string]string{"Access-Control-Allow-Headers": "Content-Type"}, nil}, - {[]string{"X-PINGOTHER", "Content-Type"}, map[string]string{"Access-Control-Allow-Headers": "X-PINGOTHER, Content-Type"}, nil}, - {[]string{"X-PINGOTHER", "Content-Type", "Authorization"}, map[string]string{"Access-Control-Allow-Headers": "X-PINGOTHER, Content-Type, Authorization"}, nil}, + {[]string{"Content-Type"}, map[string]string{"Access-Control-Allow-Headers": "Content-Type"}, []string{"Access-Control-Allow-Credentials"}}, + {[]string{"X-PINGOTHER", "Content-Type"}, map[string]string{"Access-Control-Allow-Headers": "X-PINGOTHER, Content-Type"}, []string{"Access-Control-Allow-Credentials"}}, + {[]string{"X-PINGOTHER", "Content-Type", "Authorization"}, map[string]string{"Access-Control-Allow-Headers": "X-PINGOTHER, Content-Type, Authorization"}, []string{"Access-Control-Allow-Credentials"}}, {nil, nil, []string{"Access-Control-Allow-Headers"}}, } @@ -71,3 +71,17 @@ func TestHTTPOptions_RequestHeaders_ExpectedResponseHeaders(t *testing.T) { }) } } + +func TestHTTPOptions_HeaderAuth_HasExpectedResponseHeaders(t *testing.T) { + + runTest(t, func(s *Session) { + hreq := s.HTTPRequest("OPTIONS", "/api/test/model", nil) + // Validate http response + hreq.GetResponse(t). + Equals(t, http.StatusOK, nil). + AssertHeaders(t, map[string]string{"Access-Control-Allow-Credentials": "true"}) + }, func(cfg *server.Config) { + headerAuth := "vault.method" + cfg.HeaderAuth = &headerAuth + }) +} diff --git a/test/resources.go b/test/resources.go index 0ee3816..449cc0b 100644 --- a/test/resources.go +++ b/test/resources.go @@ -104,6 +104,7 @@ var resources = map[string]resource{ const ( requestTimeout uint64 = iota noRequest + noToken ) type sequenceEvent struct {