Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cors headers #2021

Merged
merged 135 commits into from
Jun 17, 2017
Merged
Show file tree
Hide file tree
Changes from 89 commits
Commits
Show all changes
135 commits
Select commit Hold shift + click to select a range
389434c
Added function to add CORS headers and handle preflight requests.
Oct 10, 2016
cdd2ca8
Added missing required headers to the Access-Control-Allow-Headers he…
Oct 19, 2016
09282a4
Merge branch 'master' of github.com:naunga/vault into cors-headers
Oct 19, 2016
237a9af
Added config options to enable CORS and set a regexp for origins that…
Oct 19, 2016
f368d09
Reverted to master revision after moving things into more appropriate…
Oct 19, 2016
889ae29
Added wrapper function to apply CORS headers when configured to do so.
Oct 19, 2016
8c268e1
Added fields to Core and CoreConfig structs to enable CORS if desired…
Oct 19, 2016
5852bb3
Standardized on allowed origins instead of domains.
Oct 20, 2016
336ba06
Added CORS configuration options to the tests and associated files.
Oct 20, 2016
1fe3605
Merge branch 'master' of github.com:naunga/vault into cors-headers
Oct 20, 2016
88da2da
Exported the HandleCORS function and moved the invokation into the se…
Oct 20, 2016
d18ed18
This was not the right place to apply the CORS handler. Removed.
Oct 20, 2016
0f5a7fa
Added tests for requests that require CORS.
Oct 20, 2016
fd5494b
Finalized how and where to apply the CORS headers.
Oct 20, 2016
6c54ded
Setting the Origin header in order to test CORS handling.
Oct 20, 2016
9b2f66c
Removed AllowCORS() func and replaced it with CORSInfo() func. This m…
Oct 20, 2016
6747af0
Added configuration for CORS where required.
Oct 20, 2016
23490f1
Merge branch 'master' of github.com:naunga/vault into cors-headers
Oct 20, 2016
092b94d
Merge branch 'master' of github.com:naunga/vault into cors-headers
Nov 8, 2016
a8cca06
Reverting changes. CORS config to be done via a /sys endpoint.
Nov 9, 2016
6d80c03
Reverting changes. CORS config to be done via a /sys endpoint.
Nov 9, 2016
7ca440a
Refactored to align with the way things are going to be handled.
Nov 18, 2016
71048cf
Refactored to place access to the CORS settings behind the barrier in…
Nov 18, 2016
9a4c9f1
Move the logic to add the headers for CORS onto to the cors configura…
Nov 18, 2016
b5e45ba
Merge branch 'cors-headers' of github.com:naunga/vault into cors-headers
Nov 18, 2016
1171697
Merge branch 'master' of github.com:naunga/vault into cors-headers
Nov 18, 2016
71ec898
Added cors command.
Nov 23, 2016
4584a87
Moved to logic determine when to the apply the CORS headers to the Ap…
Nov 23, 2016
367a04f
Made the error returned by CORSConfig() a const.
Nov 23, 2016
ca2487f
Lots of refactoring and clean up.
Nov 23, 2016
4f6b25d
Refactoring and DRYing up.
Nov 23, 2016
3643a10
Added tests for the CORS functionality.
Nov 23, 2016
a98bba2
Initial commit.
Nov 23, 2016
e57ac5e
Initial commit.
Nov 23, 2016
5cbc9fe
Merge branch 'master' into cors-headers
Nov 23, 2016
ceb493e
Added help text.
Nov 24, 2016
9957df9
Merge branch 'master' into cors-headers
Nov 24, 2016
7aa839c
Fixed a bunch of logic bugs that were causing the server to panic aft…
Nov 28, 2016
a974a2b
Removed debug code.
Nov 29, 2016
ef2a522
Fixed logic bug.
Nov 29, 2016
b22172e
Added code to ensure the server responds correctly when the CORS requ…
Nov 29, 2016
dd06686
Skip handling the logical request for preflights.
Nov 29, 2016
0ad8ac3
Flesh out tests for CORS handler
Nov 29, 2016
796f0ef
Refactored so that the core is created with an empty CORSConfig vs. h…
Nov 29, 2016
f1a9225
Refactored ApplyHeaders() to return a 403 if the origin is not valid …
Nov 29, 2016
338fdb1
Merge branch 'master' into cors-headers
Nov 29, 2016
ec20fbc
Fixed typo in comments.
Nov 29, 2016
582189d
Removed (presently) unnecessary test.
Nov 30, 2016
121e0ed
Added documentation for CORS functionality.
Nov 30, 2016
a247952
Merge branch 'master' into cors-headers
Dec 2, 2016
50a0c11
Added a new custom header that tells the Vault server to bypass the C…
Dec 2, 2016
e5ff73f
CORS handler now applies to all paths. Added logic to bypass the CORS…
Dec 2, 2016
799cc8c
Disable() func now sets the core's CORS config to disabled and clears…
Dec 2, 2016
673365a
handleCORSDisable was setting the core's corsConfig property to nil i…
Dec 2, 2016
02edf8e
Merge branch 'master' into cors-headers
Dec 2, 2016
7122b8c
Fixed failing tests.
Dec 2, 2016
7eeba1e
Fixed a typo in a comment.
Dec 4, 2016
d3a5e45
Merge branch 'master' into cors-headers
Dec 4, 2016
5c05c80
Vary header will now be returned.
Dec 4, 2016
d809e4e
Merge branch 'master' into cors-headers
Dec 7, 2016
3bba275
Refactored code and updated documents to reflect that the allowed ori…
Dec 7, 2016
fd735ba
Merge branch 'master' into cors-headers
Dec 14, 2016
7b22c2a
Merged master and resolved conflicts.
Dec 16, 2016
3dba363
Merge branch 'master' into cors-headers
Dec 18, 2016
ff022b6
Merge branch 'master' into cors-headers
Dec 19, 2016
edab784
Merge branch 'master' into cors-headers
Dec 20, 2016
919e916
Merge branch 'master' into cors-headers
Jan 10, 2017
1e40dc8
Merge branch 'master' of github.com:naunga/vault into cors-headers
Jan 16, 2017
0031687
Merge branch 'master' into cors-headers
Jan 18, 2017
18bb96b
Merge branch 'master' into cors-headers
Jan 20, 2017
13bf4ae
Removed unnecessary header.
Jan 24, 2017
543cebd
Remove cors CLI command.
Jan 24, 2017
027e546
Merged the handleCORS() func into wrapCORSHandler(). Was not necessar…
Jan 24, 2017
d838b8e
Removed unnecessary header.
Jan 24, 2017
9d5fb2a
Move check for preflight requests up to the CORS handler where it bel…
Jan 24, 2017
75955d5
Removed unneeded struct fields. Changed pointers to slices to just sl…
Jan 24, 2017
e27b992
Refactored help text and code to reflect the fact that CORSConfig.all…
Jan 24, 2017
1dbb7e1
Merge branch 'master' of github.com:naunga/vault into cors-headers
Jan 24, 2017
8c0a7c9
New config store for api-managable configurations.
Jan 25, 2017
8ed7fe8
Added Settings field to Config struct to allow for many types of conf…
Jan 26, 2017
580ca52
Refactored to use new Config structure.
Jan 26, 2017
0ab733d
Added configStore to Core. Wired up unseal and seal tasks.
Jan 26, 2017
7645af4
Simplified things a bit.
Jan 26, 2017
1b48377
Added code to update the ConfigStore.
Jan 26, 2017
cb2ccd7
Merge branch 'master' into cors-headers
Jan 26, 2017
5f16cf7
Fixed a typo that was making the test fail.
Jan 26, 2017
70b9a9b
Added missing response headers. Renamed allowedHeaders to responseHea…
Jan 26, 2017
f70c6dc
Refactored newCORSConfig() out.
Jan 26, 2017
47f52cb
Merge branch 'master' into cors-headers
Jan 27, 2017
b7be8c5
Merge branch 'master' into cors-headers
Jan 30, 2017
0859a85
Moved logic of when to apply the CORS headers here. This makes the lo…
Jan 31, 2017
0968686
Refactored CORS handler test to reflect the current state of things.
Jan 31, 2017
26af710
It is not necessary to set the isEnabled flag on the CORSConfig. This…
Jan 31, 2017
4ac6a0b
Creating an instance of a sync.RWMutex for the CORSConfig when the co…
Jan 31, 2017
e1afa97
Renamed responseHeaders to preflightHeaders. Added 'LIST' method to l…
Jan 31, 2017
4b5a32a
Renamed handleCORSEnable, Disable, and Status to handleCORSUpdate, De…
Jan 31, 2017
3127933
Changed docs to reflect the fact that the CORS Update and Delete func…
Jan 31, 2017
2bfa2e7
Merge branch 'master' of github.com:naunga/vault into cors-headers
Jan 31, 2017
c3447f4
Refactor test to reflect the fact that the CORS Update and Delete met…
Jan 31, 2017
014ab2e
Merge branch 'master' into cors-headers
Feb 1, 2017
10a5976
Merge branch 'master' into cors-headers
Feb 5, 2017
54ce27b
Merged handleCORSRead() and corsStatusResponse() into a single func.
Feb 5, 2017
6bc49a5
Moved logic to determine when to apply the CORS headers to the http h…
Feb 7, 2017
bb5d81d
Removed the config store.
Feb 7, 2017
9afc279
Gave the test a URL that is actually valid.
Feb 7, 2017
3b0a32d
Refactored after removing the ConfigStore.
Feb 7, 2017
d8a689a
Exported the Enabled and AllowedOrigins fields of the CORSConfig stru…
Feb 7, 2017
077dfad
Updated field names.
Feb 7, 2017
7f67976
Merge branch 'master' into cors-headers
Feb 8, 2017
3d94364
Merge branch 'master' of github.com:naunga/vault into cors-headers
Feb 20, 2017
46d451b
Merge branch 'master' into cors-headers
Feb 28, 2017
fca8f36
Moved the code to put the headers on the response here. Instead of ma…
Mar 1, 2017
468fda3
CORSConfig.Get() was removed. Updated to reflect that. Fixed a typo i…
Mar 1, 2017
2a5a5ef
Removed ApplyHeaders and Get(). Applied correct locks where needed. F…
Mar 1, 2017
f6f18b8
Was missing a return after writing the headers for a preflight reques…
Mar 3, 2017
1adc2d7
Merge branch 'master' into cors-headers
Mar 3, 2017
e7f4662
Merge branch 'master' into cors-headers
Mar 8, 2017
48dd63f
Merge branch 'master' into cors-headers
Mar 8, 2017
d26b641
Merge branch 'master' into cors-headers
Mar 9, 2017
e55caae
Merge branch 'master' into cors-headers
Mar 14, 2017
7da6630
Merge branch 'master' into cors-headers
Mar 20, 2017
54314e0
Merge branch 'master' into cors-headers
Apr 10, 2017
ec019db
Merge branch 'master' of github.com:naunga/vault into cors-headers
May 6, 2017
117326a
Merge branch 'master' of github.com:naunga/vault into cors-headers
May 10, 2017
607d2d5
Updated imports.
May 10, 2017
ed209aa
Made parameter to Enable a string slice.
May 10, 2017
bd48453
Fixed a typo.
May 10, 2017
1bcdf0f
Merge branch 'master' of github.com:naunga/vault into cors-headers
May 10, 2017
b5e9b84
Corrected parameter to func Enable.
May 10, 2017
4fbeadb
Added Access-Control-Max-Age header to list of expected headers.
May 10, 2017
8c9b4e2
Enable func returns an error if the list of URLs is empty. Reworded e…
May 10, 2017
79a87ea
Merge branch 'master' of github.com:naunga/vault into cors-headers
May 10, 2017
370585b
Merge branch 'master' of github.com:naunga/vault into cors-headers
Jun 15, 2017
3403c2d
Merge branch 'master' of github.com:naunga/vault into cors-headers
Jun 15, 2017
b7be115
Merge branch 'master' of github.com:naunga/vault into cors-headers
Jun 16, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions api/sys_config_cors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package api

func (c *Sys) CORSStatus() (*CORSResponse, error) {
r := c.c.NewRequest("GET", "/v1/sys/config/cors")
resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var result CORSResponse
err = resp.DecodeJSON(&result)
return &result, err
}

func (c *Sys) ConfigureCORS(req *CORSRequest) (*CORSResponse, error) {
r := c.c.NewRequest("PUT", "/v1/sys/config/cors")
if err := r.SetJSONBody(req); err != nil {
return nil, err
}

resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var result CORSResponse
err = resp.DecodeJSON(&result)
return &result, err
}

func (c *Sys) DisableCORS() (*CORSResponse, error) {
r := c.c.NewRequest("DELETE", "/v1/sys/config/cors")

resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var result CORSResponse
err = resp.DecodeJSON(&result)
return &result, err

}

type CORSRequest struct {
AllowedOrigins string `json:"allowed_origins"`
Enabled bool `json:"enabled"`
}

type CORSResponse struct {
AllowedOrigins string `json:"allowed_origins"`
Enabled bool `json:"enabled"`
}
1 change: 0 additions & 1 deletion cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory {
Meta: *metaPtr,
}, nil
},

"server": func() (cli.Command, error) {
return &command.ServerCommand{
Meta: *metaPtr,
Expand Down
32 changes: 32 additions & 0 deletions http/cors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package http

import (
"net/http"

"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/vault"
)

func wrapCORSHandler(h http.Handler, core *vault.Core) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
corsConf := core.CORSConfig()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in the hot path since it's called on every request, so we should first check whether cors is even enabled, and immediately chain to the next handler if not. We don't really need to acquire a lock here because if cors is being toggled and requests are flying in, we don't have guaranteed ordering/timing anyways.

If cors is enabled, do we actually need to send those headers back on every request, or just with OPTIONS?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a look at the code. It should be sending headers appropriate for the request that was made.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really know which ones are appropriate for which requests, I just had the impression that you used OPTIONS to get the CORS headers, so if they're not needed for other requests we should just skip all of the code to reduce slowdown.

Copy link
Contributor Author

@naunga naunga Jan 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you look at cors.ApplyHeaders() that is exactly what we do. If CORS isn't enable, then we just return and go on to the next handler in the chain. I do see that it might be helpful to move the logic to determine if CORS in enabled up to wrapCORSHandler just to make the code more readable.

At the very least Access-Control-Allow-Origin needs to be returned with every cross-origin request. Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, and Access-Control-Max-Age need to be sent back on the response for OPTIONS (preflight) requests, and that's what I'm doing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jefferai is response to your previous comment: OPTIONS (or preflights) requests are made by the browser when a request is made with a method other than GET, HEAD, or POST and the content type is text/plain, application/x-www-form-urlencoded or multipart/form-data.

The preflight request is sent to the API by the browser to determine if the actual request is acceptable to the API. For example when a browser wants to make a LIST request the browser will make a preflight request to see if Vault will accept the LIST method.

The preflight response will include the Access-Control-Max-Age header, which sets the length of time that the preflight response should be valid so a preflight won't sent before every request.

Every browser does it a bit differently: Firefox will allow you to set the max age to 24 hours, but Chrome will only allow you to set it to a max of 10 minutes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But only for those three content types you listed, none of which we use?

As you can tell, I'm rather confused. :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jefferai CORS is a confusing subject. I've learned more about it than I ever wanted, since I started working on this PR. Let's see if I can help alleviate your confusion.

The browser will not send a preflight request to Vault (and any cross-origin resource) for the following requests:

  • GET
  • HEAD
  • POST if the content-type of the POST request is text/plain, application/x-www-form-urlencoded, or multipart/form-data.

For anything else the browser will send a preflight request to Vault (and any cross-origin resource).

Regardless of the browser's need for a preflight request the browser will expect an Access-Control-Allow-Origin header on the response from Vault and any cross-origin resource.

This is a pretty good explanation for CORS: http://restlet.com/blog/2015/12/15/understanding-and-using-cors/

Copy link

@rockerest rockerest Jan 31, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just want to comment as an interested party on the work being done here.

All of this talk of which verb or what content type is a moot point.
Because Vault requires all but the authentication requests to have the X-Vault-Token header, every request will be pre-flighted, no matter what.

This section of the Mozilla CORS page explains it in detail, but basically if you have any non-standard headers (among other things), the request is preflighted.

Note that the Authorization header does not appear in any of the whitelisted headers lists, either (in case you were thinking you could circumvent the whole CORS issue ;) ). So, unless there's a plan to make Vault stateful using the Cookie header - a bad idea - supporting CORS will be required for any useful HTTP access.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rockerest yes, exactly this! Wish I'd recalled that bit about the non-standard headers. Probably would've saved a lot of confusion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't really change the thread here: I was worried about the CORS check being done first to get it out of the hot path, and @naunga indicated that it's being done early in the function but could be moved out of it to make it more clear/explicit. I think that conversation is still valid :-)

statusCode := corsConf.ApplyHeaders(w, req)
if statusCode != http.StatusOK {
respondRaw(w, req, &logical.Response{
Data: map[string]interface{}{
logical.HTTPStatusCode: statusCode,
logical.HTTPRawBody: []byte(""),
},
})
return
}

// For pre-flight requests just send back the headers and return.
if req.Method == http.MethodOptions {
return
}

h.ServeHTTP(w, req)
return
})
}
3 changes: 2 additions & 1 deletion http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,11 @@ func Handler(core *vault.Core) http.Handler {

// Wrap the handler in another handler to trigger all help paths.
helpWrappedHandler := wrapHelpHandler(mux, core)
corsWrappedHandler := wrapCORSHandler(helpWrappedHandler, core)

// Wrap the help wrapped handler with another layer with a generic
// handler
genericWrappedHandler := wrapGenericHandler(helpWrappedHandler)
genericWrappedHandler := wrapGenericHandler(corsWrappedHandler)

return genericWrappedHandler
}
Expand Down
86 changes: 86 additions & 0 deletions http/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,92 @@ import (
"github.com/hashicorp/vault/vault"
)

func TestHandler_cors(t *testing.T) {
core, _, _ := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
defer ln.Close()

// Enable CORS and allow from any origin for testing.
corsConfig := core.CORSConfig()
err := corsConfig.Enable(addr)
if err != nil {
t.Fatalf("Error enabling CORS: %s", err)
}

req, err := http.NewRequest(http.MethodOptions, addr+"/v1/", nil)
if err != nil {
t.Fatalf("err: %s", err)
}
req.Header.Set("Origin", "BAD ORIGIN")

// Requests from unacceptable origins will be rejected with a 403.
client := cleanhttp.DefaultClient()
resp, err := client.Do(req)
if err != nil {
t.Fatalf("err: %s", err)
}

if resp.StatusCode != http.StatusForbidden {
t.Fatalf("Bad status:\nexpected: 403 Forbidden\nactual: %s", resp.Status)
}

//
// Test preflight requests
//

// Set a valid origin
req.Header.Set("Origin", addr)

// Server should NOT accept arbitrary methods.
req.Header.Set("Access-Control-Request-Method", "FOO")

client = cleanhttp.DefaultClient()
resp, err = client.Do(req)
if err != nil {
t.Fatalf("err: %s", err)
}

// Fail if an arbitrary method is accepted.
if resp.StatusCode != http.StatusMethodNotAllowed {
t.Fatalf("Bad status:\nexpected: 405 Method Not Allowed\nactual: %s", resp.Status)
}

// Server SHOULD accept acceptable methods.
req.Header.Set("Access-Control-Request-Method", http.MethodPost)

client = cleanhttp.DefaultClient()
resp, err = client.Do(req)
if err != nil {
t.Fatalf("err: %s", err)
}

// Fail if an acceptable method is NOT accepted.
if resp.StatusCode != http.StatusOK {
t.Fatalf("Bad status:\nexpected: 200 OK\nactual: %s", resp.Status)
}

//
// Test that the CORS headers are applied correctly.
//
expHeaders := map[string]string{
"Access-Control-Allow-Origin": addr,
"Access-Control-Allow-Headers": "origin,content-type,cache-control,accept,options,authorization,x-requested-with,x-vault-token",
"Access-Control-Allow-Credentials": "true",
"Vary": "Origin",
}

for expHeader, expected := range expHeaders {
actual := resp.Header.Get(expHeader)
if actual == "" {
t.Fatalf("bad:\nHeader: %#v was not on response.", expHeader)
}

if actual != expected {
t.Fatalf("bad:\nExpected: %#v\nActual: %#v\n", expected, actual)
}
}
}

func TestHandler_CacheControlNoStore(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
Expand Down
6 changes: 6 additions & 0 deletions http/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"regexp"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -55,6 +56,11 @@ func testHttpData(t *testing.T, method string, token string, addr string, body i
t.Fatalf("err: %s", err)
}

// Get the address of the local listener in order to attach it to an Origin header.
// This will allow for the testing of requests that require CORS, without using a browser.
hostURLRegexp, _ := regexp.Compile("http[s]?://.+:[0-9]+")
req.Header.Set("Origin", hostURLRegexp.FindString(addr))

req.Header.Set("Content-Type", "application/json")

if len(token) != 0 {
Expand Down
1 change: 1 addition & 0 deletions http/logical.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func buildLogicalRequest(core *vault.Core, w http.ResponseWriter, r *http.Reques
op = logical.UpdateOperation
case "LIST":
op = logical.ListOperation
case "OPTIONS":
default:
return nil, http.StatusMethodNotAllowed, nil
}
Expand Down
Loading