From 27e584c2ce702eaa9665f9aed795ddc790c47eec Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Sat, 17 Jun 2017 01:26:25 -0400 Subject: [PATCH] Fix up CORS. Ref #2021 --- command/rekey_test.go | 3 +- vault/core.go | 10 +- vault/cors.go | 59 ++++++---- vault/logical_system.go | 50 ++++---- vault/logical_system_test.go | 22 ++-- website/source/api/system/config-cors.html.md | 87 ++++++++++++++ .../source/docs/http/sys-config-cors.html.md | 109 ------------------ website/source/layouts/api.erb | 3 + 8 files changed, 175 insertions(+), 168 deletions(-) create mode 100644 website/source/api/system/config-cors.html.md delete mode 100644 website/source/docs/http/sys-config-cors.html.md diff --git a/command/rekey_test.go b/command/rekey_test.go index 7911111c4d9a..c199dd2fd8f5 100644 --- a/command/rekey_test.go +++ b/command/rekey_test.go @@ -199,7 +199,8 @@ func TestRekey_init_pgp(t *testing.T) { MaxLeaseTTLVal: time.Hour * 24 * 32, }, } - sysBackend, err := vault.NewSystemBackend(core, bc) + sysBE := vault.NewSystemBackend(core) + sysBackend, err := sysBE.Backend.Setup(bc) if err != nil { t.Fatal(err) } diff --git a/vault/core.go b/vault/core.go index b940dbc6ca91..355f55e524ce 100644 --- a/vault/core.go +++ b/vault/core.go @@ -450,11 +450,13 @@ func NewCore(conf *CoreConfig) (*Core, error) { clusterName: conf.ClusterName, clusterListenerShutdownCh: make(chan struct{}), clusterListenerShutdownSuccessCh: make(chan struct{}), - corsConfig: &CORSConfig{}, clusterPeerClusterAddrsCache: cache.New(3*heartbeatInterval, time.Second), enableMlock: !conf.DisableMlock, } + // Load CORS config and provide core + c.corsConfig = &CORSConfig{core: c} + // Wrap the physical backend in a cache layer if enabled and not already wrapped if _, isCache := conf.Physical.(*physical.Cache); !conf.DisableCache && !isCache { c.physical = physical.NewCache(conf.Physical, conf.CacheSize, conf.Logger) @@ -513,7 +515,8 @@ func NewCore(conf *CoreConfig) (*Core, error) { } logicalBackends["cubbyhole"] = CubbyholeBackendFactory logicalBackends["system"] = func(config *logical.BackendConfig) (logical.Backend, error) { - return NewSystemBackend(c, config) + b := NewSystemBackend(c) + return b.Backend.Setup(config) } c.logicalBackends = logicalBackends @@ -1368,9 +1371,6 @@ func (c *Core) preSeal() error { if err := c.teardownPolicyStore(); err != nil { result = multierror.Append(result, errwrap.Wrapf("error tearing down policy store: {{err}}", err)) } - if err := c.saveCORSConfig(); err != nil { - result = multierror.Append(result, errwrap.Wrapf("error tearing down CORS config: {{err}}", err)) - } if err := c.stopRollback(); err != nil { result = multierror.Append(result, errwrap.Wrapf("error stopping rollback: {{err}}", err)) } diff --git a/vault/cors.go b/vault/cors.go index 288c57b498f1..c1fd961284c2 100644 --- a/vault/cors.go +++ b/vault/cors.go @@ -4,24 +4,36 @@ import ( "errors" "fmt" "sync" + "sync/atomic" "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" ) -var errCORSNotConfigured = errors.New("CORS is not configured") +const ( + CORSDisabled uint32 = iota + CORSEnabled +) // CORSConfig stores the state of the CORS configuration. type CORSConfig struct { - sync.RWMutex - Enabled bool `json:"enabled"` - AllowedOrigins []string `json:"allowed_origins"` + sync.RWMutex `json:"-"` + core *Core + Enabled uint32 `json:"enabled"` + AllowedOrigins []string `json:"allowed_origins,omitempty"` } func (c *Core) saveCORSConfig() error { view := c.systemBarrierView.SubView("config/") - entry, err := logical.StorageEntryJSON("cors", c.corsConfig) + localConfig := &CORSConfig{ + Enabled: atomic.LoadUint32(&c.corsConfig.Enabled), + } + c.corsConfig.RLock() + localConfig.AllowedOrigins = c.corsConfig.AllowedOrigins + c.corsConfig.RUnlock() + + entry, err := logical.StorageEntryJSON("cors", localConfig) if err != nil { return fmt.Errorf("failed to create CORS config entry: %v", err) } @@ -33,6 +45,7 @@ func (c *Core) saveCORSConfig() error { return nil } +// This should only be called with the core state lock held for writing func (c *Core) loadCORSConfig() error { view := c.systemBarrierView.SubView("config/") @@ -45,10 +58,14 @@ func (c *Core) loadCORSConfig() error { return nil } - err = out.DecodeJSON(c.corsConfig) + newConfig := new(CORSConfig) + err = out.DecodeJSON(newConfig) if err != nil { return err } + newConfig.core = c + + c.corsConfig = newConfig return nil } @@ -65,38 +82,40 @@ func (c *CORSConfig) Enable(urls []string) error { } c.Lock() - defer c.Unlock() - c.AllowedOrigins = urls - c.Enabled = true + c.Unlock() - return nil + atomic.StoreUint32(&c.Enabled, CORSEnabled) + + return c.core.saveCORSConfig() } // IsEnabled returns the value of CORSConfig.isEnabled func (c *CORSConfig) IsEnabled() bool { - c.RLock() - defer c.RUnlock() - - return c.Enabled + return atomic.LoadUint32(&c.Enabled) == CORSEnabled } // Disable sets CORS to disabled and clears the allowed origins -func (c *CORSConfig) Disable() { +func (c *CORSConfig) Disable() error { + atomic.StoreUint32(&c.Enabled, CORSDisabled) c.Lock() - defer c.Unlock() - - c.Enabled = false - c.AllowedOrigins = []string{} + c.AllowedOrigins = []string(nil) + c.Unlock() + return c.core.saveCORSConfig() } // IsValidOrigin determines if the origin of the request is allowed to make // cross-origin requests based on the CORSConfig. func (c *CORSConfig) IsValidOrigin(origin string) bool { + // If we aren't enabling CORS then all origins are valid + if !c.IsEnabled() { + return true + } + c.RLock() defer c.RUnlock() - if c.AllowedOrigins == nil { + if len(c.AllowedOrigins) == 0 { return false } diff --git a/vault/logical_system.go b/vault/logical_system.go index 20573f6a9b74..39b69c4c350f 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -44,7 +44,7 @@ var ( } ) -func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backend, error) { +func NewSystemBackend(core *Core) *SystemBackend { b := &SystemBackend{ Core: core, } @@ -62,7 +62,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen "replication/primary/secondary-token", "replication/reindex", "rotate", - "config/*", + "config/cors", "config/auditing/*", "plugins/catalog/*", "revoke-prefix/*", @@ -110,7 +110,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen }, "allowed_origins": &framework.FieldSchema{ Type: framework.TypeCommaStringSlice, - Description: "A comma-separated list of origins that may make cross-origin requests.", + Description: "A comma-separated string or array of strings indicating origins that may make cross-origin requests.", }, }, @@ -823,50 +823,50 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen b.Backend.Invalidate = b.invalidate - return b.Backend.Setup(config) + return b } // SystemBackend implements logical.Backend and is used to interact with // the core of the system. This backend is hardcoded to exist at the "sys" // prefix. Conceptually it is similar to procfs on Linux. type SystemBackend struct { - Core *Core - Backend *framework.Backend + *framework.Backend + Core *Core } // handleCORSRead returns the current CORS configuration func (b *SystemBackend) handleCORSRead(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { corsConf := b.Core.corsConfig - if corsConf == nil { - return nil, errCORSNotConfigured - } - return &logical.Response{ + enabled := corsConf.IsEnabled() + + resp := &logical.Response{ Data: map[string]interface{}{ - "enabled": corsConf.Enabled, - "allowed_origins": strings.Join(corsConf.AllowedOrigins, ","), + "enabled": enabled, }, - }, nil + } + + if enabled { + corsConf.RLock() + resp.Data["allowed_origins"] = corsConf.AllowedOrigins + corsConf.RUnlock() + } + + return resp, nil } -// handleCORSUpdate sets the list of origins that are allowed -// to make cross-origin requests and sets the CORS enabled flag to true +// handleCORSUpdate sets the list of origins that are allowed to make +// cross-origin requests and sets the CORS enabled flag to true func (b *SystemBackend) handleCORSUpdate(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { origins := d.Get("allowed_origins").([]string) - err := b.Core.corsConfig.Enable(origins) - if err != nil { - return nil, err - } - - return nil, nil + return nil, b.Core.corsConfig.Enable(origins) } -// handleCORSDelete clears the allowed origins and sets the CORS enabled flag to false +// handleCORSDelete clears the allowed origins and sets the CORS enabled flag +// to false func (b *SystemBackend) handleCORSDelete(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - b.Core.CORSConfig().Disable() - - return nil, nil + return nil, b.Core.corsConfig.Disable() } func (b *SystemBackend) handleTidyLeases(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 87df8e038096..48445976677d 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -49,6 +49,9 @@ func TestSystemBackend_RootPaths(t *testing.T) { func TestSystemConfigCORS(t *testing.T) { b := testSystemBackend(t) + _, barrier, _ := mockBarrier(t) + view := NewBarrierView(barrier, "") + b.(*SystemBackend).Core.systemBarrierView = view req := logical.TestRequest(t, logical.UpdateOperation, "config/cors") req.Data["allowed_origins"] = "http://www.example.com" @@ -60,7 +63,7 @@ func TestSystemConfigCORS(t *testing.T) { expected := &logical.Response{ Data: map[string]interface{}{ "enabled": true, - "allowed_origins": "http://www.example.com", + "allowed_origins": []string{"http://www.example.com"}, }, } @@ -71,7 +74,7 @@ func TestSystemConfigCORS(t *testing.T) { } if !reflect.DeepEqual(actual, expected) { - t.Fatalf("UPDATE FAILED -- bad: %#v", actual) + t.Fatalf("bad: %#v", actual) } req = logical.TestRequest(t, logical.DeleteOperation, "config/cors") @@ -88,8 +91,7 @@ func TestSystemConfigCORS(t *testing.T) { expected = &logical.Response{ Data: map[string]interface{}{ - "enabled": false, - "allowed_origins": "", + "enabled": false, }, } @@ -980,7 +982,8 @@ func TestSystemBackend_revokePrefixAuth(t *testing.T) { MaxLeaseTTLVal: time.Hour * 24 * 32, }, } - b, err := NewSystemBackend(core, bc) + be := NewSystemBackend(core) + b, err := be.Backend.Setup(bc) if err != nil { t.Fatal(err) } @@ -1043,7 +1046,8 @@ func TestSystemBackend_revokePrefixAuth_origUrl(t *testing.T) { MaxLeaseTTLVal: time.Hour * 24 * 32, }, } - b, err := NewSystemBackend(core, bc) + be := NewSystemBackend(core) + b, err := be.Backend.Setup(bc) if err != nil { t.Fatal(err) } @@ -1578,7 +1582,8 @@ func testSystemBackend(t *testing.T) logical.Backend { }, } - b, err := NewSystemBackend(c, bc) + b := NewSystemBackend(c) + _, err := b.Backend.Setup(bc) if err != nil { t.Fatal(err) } @@ -1596,7 +1601,8 @@ func testCoreSystemBackend(t *testing.T) (*Core, logical.Backend, string) { }, } - b, err := NewSystemBackend(c, bc) + b := NewSystemBackend(c) + _, err := b.Backend.Setup(bc) if err != nil { t.Fatal(err) } diff --git a/website/source/api/system/config-cors.html.md b/website/source/api/system/config-cors.html.md new file mode 100644 index 000000000000..659a74a38cb4 --- /dev/null +++ b/website/source/api/system/config-cors.html.md @@ -0,0 +1,87 @@ +--- +layout: "api" +page_title: "/sys/config/cors - HTTP API" +sidebar_current: "docs-http-system-config-cors" +description: |- + The '/sys/config/cors' endpoint configures how the Vault server responds to cross-origin requests. +--- + +# `/sys/config/cors` + +The `/sys/config/cors` endpoint is used to configure CORS settings. + +- **`sudo` required** – All CORS endpoints require `sudo` capability in + addition to any path-specific capabilities. + +## Read CORS Settings + +This endpoint returns the current CORS configuration. + +| Method | Path | Produces | +| :------- | :--------------------------- | :--------------------- | +| `GET` | `/sys/config/cors` | `200 application/json` | + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + https://vault.rocks/v1/sys/config/cors +``` + +### Sample Response + +```json +{ + "enabled": true, + "allowed_origins": "http://www.example.com" +} +``` + +## Configure CORS Settings + +This endpoint allows configuring the origins that are permitted to make +cross-origin requests. + +| Method | Path | Produces | +| :------- | :--------------------------- | :--------------------- | +| `PUT` | `/sys/config/cors` | `204 (empty body)` | + +### Parameters + +- `allowed_origins` `(string or string array: "" or [])` – A wildcard (`*`), comma-delimited string, or array of strings specifying the origins that are permitted to make cross-origin requests. + +### Sample Payload + +```json +{ + "allowed_origins": "*" +} +``` + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request PUT \ + --data @payload.json \ + https://vault.rocks/v1/sys/config/cors +``` + +## Delete CORS Settings + +This endpoint removes any CORS configuration. + +| Method | Path | Produces | +| :------- | :--------------------------- | :--------------------- | +| `DELETE` | `/sys/config/cors` | `204 (empty body)` | + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request DELETE \ + https://vault.rocks/v1/sys/config/cors +``` diff --git a/website/source/docs/http/sys-config-cors.html.md b/website/source/docs/http/sys-config-cors.html.md deleted file mode 100644 index 05755f996b4f..000000000000 --- a/website/source/docs/http/sys-config-cors.html.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -layout: "http" -page_title: "HTTP API: /sys/config/cors" -sidebar_current: "docs-http-config-cors" -description: |- - The '/sys/config/cors' endpoint configures how the Vault server responds to cross-origin requests. ---- - -# /sys/config/cors - -This is a protected path, therefore all requests require a token with `root` -policy or `sudo` capability on the path. - -## GET - -
-
Description
-
- Returns the current CORS configuration. -
- -
Method
-
GET
- -
URL
-
`/sys/config/cors`
- -
Parameters
-
- None -
- -
Returns
-
- - ```javascript - { - "enabled": true, - "allowed_origins": "http://www.example.com" - } - ``` - - Sample response when CORS is disabled. - - ```javascript - { - "enabled": false, - "allowed_origins": "" - } - ``` -
-
- -## PUT - -
-
Description
-
- Configures the Vault server to return CORS headers for origins that are - permitted to make cross-origin requests based on the `allowed_origins` - parameter. -
- -
Method
-
PUT
- -
URL
-
`/sys/config/cors`
- -
Parameters
-
-
    -
  • - allowed_origins - required - Valid values are either a wildcard (*) or a comma-separated list of - exact origins that are permitted to make cross-origin requests. -
  • -
-
- -
Returns
-
`204` response code. -
-
- -## DELETE - -
-
Description
-
- Disables the CORS functionality of the Vault server. -
- -
Method
-
DELETE
- -
URL
-
`/sys/config/cors`
- -
Parameters
-
- None -
- -
Returns
-
`204` response code. -
-
diff --git a/website/source/layouts/api.erb b/website/source/layouts/api.erb index 016aa4265c4a..1c80cd3e8d20 100644 --- a/website/source/layouts/api.erb +++ b/website/source/layouts/api.erb @@ -108,6 +108,9 @@ > /sys/config/auditing + > + /sys/config/cors + > /sys/generate-root