Fix up CORS.

Ref #2021
This commit is contained in:
Jeff Mitchell 2017-06-17 01:26:25 -04:00
parent 0303f51b68
commit cf7d56e8f3
8 changed files with 176 additions and 169 deletions

View File

@ -199,7 +199,8 @@ func TestRekey_init_pgp(t *testing.T) {
MaxLeaseTTLVal: time.Hour * 24 * 32, MaxLeaseTTLVal: time.Hour * 24 * 32,
}, },
} }
sysBackend, err := vault.NewSystemBackend(core, bc) sysBE := vault.NewSystemBackend(core)
sysBackend, err := sysBE.Backend.Setup(bc)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -450,11 +450,13 @@ func NewCore(conf *CoreConfig) (*Core, error) {
clusterName: conf.ClusterName, clusterName: conf.ClusterName,
clusterListenerShutdownCh: make(chan struct{}), clusterListenerShutdownCh: make(chan struct{}),
clusterListenerShutdownSuccessCh: make(chan struct{}), clusterListenerShutdownSuccessCh: make(chan struct{}),
corsConfig: &CORSConfig{},
clusterPeerClusterAddrsCache: cache.New(3*heartbeatInterval, time.Second), clusterPeerClusterAddrsCache: cache.New(3*heartbeatInterval, time.Second),
enableMlock: !conf.DisableMlock, 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 // Wrap the physical backend in a cache layer if enabled and not already wrapped
if _, isCache := conf.Physical.(*physical.Cache); !conf.DisableCache && !isCache { if _, isCache := conf.Physical.(*physical.Cache); !conf.DisableCache && !isCache {
c.physical = physical.NewCache(conf.Physical, conf.CacheSize, conf.Logger) c.physical = physical.NewCache(conf.Physical, conf.CacheSize, conf.Logger)
@ -513,7 +515,8 @@ func NewCore(conf *CoreConfig) (*Core, error) {
} }
logicalBackends["cubbyhole"] = CubbyholeBackendFactory logicalBackends["cubbyhole"] = CubbyholeBackendFactory
logicalBackends["system"] = func(config *logical.BackendConfig) (logical.Backend, error) { logicalBackends["system"] = func(config *logical.BackendConfig) (logical.Backend, error) {
return NewSystemBackend(c, config) b := NewSystemBackend(c)
return b.Backend.Setup(config)
} }
c.logicalBackends = logicalBackends c.logicalBackends = logicalBackends
@ -1368,9 +1371,6 @@ func (c *Core) preSeal() error {
if err := c.teardownPolicyStore(); err != nil { if err := c.teardownPolicyStore(); err != nil {
result = multierror.Append(result, errwrap.Wrapf("error tearing down policy store: {{err}}", err)) 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 { if err := c.stopRollback(); err != nil {
result = multierror.Append(result, errwrap.Wrapf("error stopping rollback: {{err}}", err)) result = multierror.Append(result, errwrap.Wrapf("error stopping rollback: {{err}}", err))
} }

View File

@ -4,24 +4,36 @@ import (
"errors" "errors"
"fmt" "fmt"
"sync" "sync"
"sync/atomic"
"github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/helper/strutil"
"github.com/hashicorp/vault/logical" "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. // CORSConfig stores the state of the CORS configuration.
type CORSConfig struct { type CORSConfig struct {
sync.RWMutex sync.RWMutex `json:"-"`
Enabled bool `json:"enabled"` core *Core
AllowedOrigins []string `json:"allowed_origins"` Enabled uint32 `json:"enabled"`
AllowedOrigins []string `json:"allowed_origins,omitempty"`
} }
func (c *Core) saveCORSConfig() error { func (c *Core) saveCORSConfig() error {
view := c.systemBarrierView.SubView("config/") 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 { if err != nil {
return fmt.Errorf("failed to create CORS config entry: %v", err) return fmt.Errorf("failed to create CORS config entry: %v", err)
} }
@ -33,6 +45,7 @@ func (c *Core) saveCORSConfig() error {
return nil return nil
} }
// This should only be called with the core state lock held for writing
func (c *Core) loadCORSConfig() error { func (c *Core) loadCORSConfig() error {
view := c.systemBarrierView.SubView("config/") view := c.systemBarrierView.SubView("config/")
@ -45,10 +58,14 @@ func (c *Core) loadCORSConfig() error {
return nil return nil
} }
err = out.DecodeJSON(c.corsConfig) newConfig := new(CORSConfig)
err = out.DecodeJSON(newConfig)
if err != nil { if err != nil {
return err return err
} }
newConfig.core = c
c.corsConfig = newConfig
return nil return nil
} }
@ -65,38 +82,40 @@ func (c *CORSConfig) Enable(urls []string) error {
} }
c.Lock() c.Lock()
defer c.Unlock()
c.AllowedOrigins = urls 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 // IsEnabled returns the value of CORSConfig.isEnabled
func (c *CORSConfig) IsEnabled() bool { func (c *CORSConfig) IsEnabled() bool {
c.RLock() return atomic.LoadUint32(&c.Enabled) == CORSEnabled
defer c.RUnlock()
return c.Enabled
} }
// Disable sets CORS to disabled and clears the allowed origins // 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() c.Lock()
defer c.Unlock() c.AllowedOrigins = []string(nil)
c.Unlock()
c.Enabled = false return c.core.saveCORSConfig()
c.AllowedOrigins = []string{}
} }
// IsValidOrigin determines if the origin of the request is allowed to make // IsValidOrigin determines if the origin of the request is allowed to make
// cross-origin requests based on the CORSConfig. // cross-origin requests based on the CORSConfig.
func (c *CORSConfig) IsValidOrigin(origin string) bool { 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() c.RLock()
defer c.RUnlock() defer c.RUnlock()
if c.AllowedOrigins == nil { if len(c.AllowedOrigins) == 0 {
return false return false
} }

View File

@ -44,7 +44,7 @@ var (
} }
) )
func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backend, error) { func NewSystemBackend(core *Core) *SystemBackend {
b := &SystemBackend{ b := &SystemBackend{
Core: core, Core: core,
} }
@ -62,7 +62,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen
"replication/primary/secondary-token", "replication/primary/secondary-token",
"replication/reindex", "replication/reindex",
"rotate", "rotate",
"config/*", "config/cors",
"config/auditing/*", "config/auditing/*",
"plugins/catalog/*", "plugins/catalog/*",
"revoke-prefix/*", "revoke-prefix/*",
@ -110,7 +110,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen
}, },
"allowed_origins": &framework.FieldSchema{ "allowed_origins": &framework.FieldSchema{
Type: framework.TypeCommaStringSlice, 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 b.Backend.Invalidate = b.invalidate
return b.Backend.Setup(config) return b
} }
// SystemBackend implements logical.Backend and is used to interact with // SystemBackend implements logical.Backend and is used to interact with
// the core of the system. This backend is hardcoded to exist at the "sys" // the core of the system. This backend is hardcoded to exist at the "sys"
// prefix. Conceptually it is similar to procfs on Linux. // prefix. Conceptually it is similar to procfs on Linux.
type SystemBackend struct { type SystemBackend struct {
Core *Core *framework.Backend
Backend *framework.Backend Core *Core
} }
// handleCORSRead returns the current CORS configuration // handleCORSRead returns the current CORS configuration
func (b *SystemBackend) handleCORSRead(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { func (b *SystemBackend) handleCORSRead(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
corsConf := b.Core.corsConfig corsConf := b.Core.corsConfig
if corsConf == nil {
return nil, errCORSNotConfigured enabled := corsConf.IsEnabled()
resp := &logical.Response{
Data: map[string]interface{}{
"enabled": enabled,
},
} }
return &logical.Response{ if enabled {
Data: map[string]interface{}{ corsConf.RLock()
"enabled": corsConf.Enabled, resp.Data["allowed_origins"] = corsConf.AllowedOrigins
"allowed_origins": strings.Join(corsConf.AllowedOrigins, ","), corsConf.RUnlock()
}, }
}, nil
return resp, nil
} }
// handleCORSUpdate sets the list of origins that are allowed // handleCORSUpdate sets the list of origins that are allowed to make
// to make cross-origin requests and sets the CORS enabled flag to true // cross-origin requests and sets the CORS enabled flag to true
func (b *SystemBackend) handleCORSUpdate(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { func (b *SystemBackend) handleCORSUpdate(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
origins := d.Get("allowed_origins").([]string) origins := d.Get("allowed_origins").([]string)
err := b.Core.corsConfig.Enable(origins) return nil, b.Core.corsConfig.Enable(origins)
if err != nil {
return nil, err
}
return nil, nil
} }
// 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) { func (b *SystemBackend) handleCORSDelete(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
b.Core.CORSConfig().Disable() return nil, b.Core.corsConfig.Disable()
return nil, nil
} }
func (b *SystemBackend) handleTidyLeases(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { func (b *SystemBackend) handleTidyLeases(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {

View File

@ -49,6 +49,9 @@ func TestSystemBackend_RootPaths(t *testing.T) {
func TestSystemConfigCORS(t *testing.T) { func TestSystemConfigCORS(t *testing.T) {
b := testSystemBackend(t) b := testSystemBackend(t)
_, barrier, _ := mockBarrier(t)
view := NewBarrierView(barrier, "")
b.(*SystemBackend).Core.systemBarrierView = view
req := logical.TestRequest(t, logical.UpdateOperation, "config/cors") req := logical.TestRequest(t, logical.UpdateOperation, "config/cors")
req.Data["allowed_origins"] = "http://www.example.com" req.Data["allowed_origins"] = "http://www.example.com"
@ -60,7 +63,7 @@ func TestSystemConfigCORS(t *testing.T) {
expected := &logical.Response{ expected := &logical.Response{
Data: map[string]interface{}{ Data: map[string]interface{}{
"enabled": true, "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) { 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") req = logical.TestRequest(t, logical.DeleteOperation, "config/cors")
@ -88,8 +91,7 @@ func TestSystemConfigCORS(t *testing.T) {
expected = &logical.Response{ expected = &logical.Response{
Data: map[string]interface{}{ Data: map[string]interface{}{
"enabled": false, "enabled": false,
"allowed_origins": "",
}, },
} }
@ -980,7 +982,8 @@ func TestSystemBackend_revokePrefixAuth(t *testing.T) {
MaxLeaseTTLVal: time.Hour * 24 * 32, MaxLeaseTTLVal: time.Hour * 24 * 32,
}, },
} }
b, err := NewSystemBackend(core, bc) be := NewSystemBackend(core)
b, err := be.Backend.Setup(bc)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -1043,7 +1046,8 @@ func TestSystemBackend_revokePrefixAuth_origUrl(t *testing.T) {
MaxLeaseTTLVal: time.Hour * 24 * 32, MaxLeaseTTLVal: time.Hour * 24 * 32,
}, },
} }
b, err := NewSystemBackend(core, bc) be := NewSystemBackend(core)
b, err := be.Backend.Setup(bc)
if err != nil { if err != nil {
t.Fatal(err) 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 { if err != nil {
t.Fatal(err) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

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

View File

@ -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
<dl>
<dt>Description</dt>
<dd>
Returns the current CORS configuration.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/sys/config/cors`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"enabled": true,
"allowed_origins": "http://www.example.com"
}
```
Sample response when CORS is disabled.
```javascript
{
"enabled": false,
"allowed_origins": ""
}
```
</dd>
</dl>
## PUT
<dl>
<dt>Description</dt>
<dd>
Configures the Vault server to return CORS headers for origins that are
permitted to make cross-origin requests based on the `allowed_origins`
parameter.
</dd>
<dt>Method</dt>
<dd>PUT</dd>
<dt>URL</dt>
<dd>`/sys/config/cors`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">allowed_origins</span>
<span class="param-flags">required</span>
Valid values are either a wildcard (*) or a comma-separated list of
exact origins that are permitted to make cross-origin requests.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>`204` response code.
</dd>
</dl>
## DELETE
<dl>
<dt>Description</dt>
<dd>
Disables the CORS functionality of the Vault server.
</dd>
<dt>Method</dt>
<dd>DELETE</dd>
<dt>URL</dt>
<dd>`/sys/config/cors`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>`204` response code.
</dd>
</dl>

View File

@ -108,6 +108,9 @@
<li<%= sidebar_current("docs-http-system-config-auditing") %>> <li<%= sidebar_current("docs-http-system-config-auditing") %>>
<a href="/api/system/config-auditing.html"><tt>/sys/config/auditing</tt></a> <a href="/api/system/config-auditing.html"><tt>/sys/config/auditing</tt></a>
</li> </li>
<li<%= sidebar_current("docs-http-system-config-cors") %>>
<a href="/api/system/config-cors.html"><tt>/sys/config/cors</tt></a>
</li>
<li<%= sidebar_current("docs-http-system-generate-root") %>> <li<%= sidebar_current("docs-http-system-generate-root") %>>
<a href="/api/system/generate-root.html"><tt>/sys/generate-root</tt></a> <a href="/api/system/generate-root.html"><tt>/sys/generate-root</tt></a>
</li> </li>