mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +00:00 
			
		
		
		
	OSS: Adding UI handlers and configurable headers (#390)
* adding UI handlers and UI header configuration * forcing specific static headers * properly getting UI config value from config/environment * fixing formatting in stub UI text * use http.Header * case-insensitive X-Vault header check * fixing var name * wrap both stubbed and real UI in header handler * adding test for >1 keys
This commit is contained in:
		 Chris Hoffman
					Chris Hoffman
				
			
				
					committed by
					
						 Matthew Irish
						Matthew Irish
					
				
			
			
				
	
			
			
			 Matthew Irish
						Matthew Irish
					
				
			
						parent
						
							2c2f0d853f
						
					
				
				
					commit
					af33ece136
				
			
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -65,6 +65,7 @@ tags | ||||
| ui/dist | ||||
| ui/tmp | ||||
| ui/root | ||||
| http/bindata_assetfs.go | ||||
|  | ||||
| # dependencies | ||||
| ui/node_modules | ||||
|   | ||||
| @@ -452,6 +452,7 @@ func (c *ServerCommand) Run(args []string) int { | ||||
| 		ClusterName:        config.ClusterName, | ||||
| 		CacheSize:          config.CacheSize, | ||||
| 		PluginDirectory:    config.PluginDirectory, | ||||
| 		EnableUI:           config.EnableUI, | ||||
| 		EnableRaw:          config.EnableRawEndpoint, | ||||
| 	} | ||||
| 	if c.flagDev { | ||||
| @@ -607,6 +608,16 @@ CLUSTER_SYNTHESIS_COMPLETE: | ||||
| 		coreConfig.ClusterAddr = u.String() | ||||
| 	} | ||||
|  | ||||
| 	// Override the UI enabling config by the environment variable | ||||
| 	if enableUI := os.Getenv("VAULT_UI"); enableUI != "" { | ||||
| 		var err error | ||||
| 		coreConfig.EnableUI, err = strconv.ParseBool(enableUI) | ||||
| 		if err != nil { | ||||
| 			c.UI.Output("Error parsing the environment variable VAULT_UI") | ||||
| 			return 1 | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Initialize the core | ||||
| 	core, newCoreError := vault.NewCore(coreConfig) | ||||
| 	if newCoreError != nil { | ||||
|   | ||||
| @@ -6,9 +6,11 @@ import ( | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/elazarl/go-bindata-assetfs" | ||||
| 	"github.com/hashicorp/errwrap" | ||||
| 	cleanhttp "github.com/hashicorp/go-cleanhttp" | ||||
| 	"github.com/hashicorp/vault/helper/consts" | ||||
| @@ -54,6 +56,9 @@ const ( | ||||
|  | ||||
| var ( | ||||
| 	ReplicationStaleReadTimeout = 2 * time.Second | ||||
|  | ||||
| 	// Set to false by stub_asset if the ui build tag isn't enabled | ||||
| 	uiBuiltIn = true | ||||
| ) | ||||
|  | ||||
| // Handler returns an http.Handler for the API. This can be used on | ||||
| @@ -82,6 +87,14 @@ func Handler(core *vault.Core) http.Handler { | ||||
| 	} | ||||
| 	mux.Handle("/v1/sys/", handleRequestForwarding(core, handleLogical(core, false, nil))) | ||||
| 	mux.Handle("/v1/", handleRequestForwarding(core, handleLogical(core, false, nil))) | ||||
| 	if core.UIEnabled() == true { | ||||
| 		if uiBuiltIn { | ||||
| 			mux.Handle("/ui/", http.StripPrefix("/ui/", handleUIHeaders(core, handleUI(http.FileServer(&UIAssetWrapper{FileSystem: assetFS()}))))) | ||||
| 		} else { | ||||
| 			mux.Handle("/ui/", handleUIHeaders(core, handleUIStub())) | ||||
| 		} | ||||
| 		mux.Handle("/", handleRootRedirect()) | ||||
| 	} | ||||
|  | ||||
| 	// Wrap the handler in another handler to trigger all help paths. | ||||
| 	helpWrappedHandler := wrapHelpHandler(mux, core) | ||||
| @@ -145,6 +158,72 @@ func stripPrefix(prefix, path string) (string, bool) { | ||||
| 	return path, true | ||||
| } | ||||
|  | ||||
| func handleUIHeaders(core *vault.Core, h http.Handler) http.Handler { | ||||
| 	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | ||||
| 		header := w.Header() | ||||
|  | ||||
| 		userHeaders, err := core.UIHeaders() | ||||
| 		if err != nil { | ||||
| 			respondError(w, http.StatusInternalServerError, err) | ||||
| 			return | ||||
| 		} | ||||
| 		if userHeaders != nil { | ||||
| 			for k := range userHeaders { | ||||
| 				v := userHeaders.Get(k) | ||||
| 				header.Set(k, v) | ||||
| 			} | ||||
| 		} | ||||
| 		h.ServeHTTP(w, req) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func handleUI(h http.Handler) http.Handler { | ||||
| 	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | ||||
| 		h.ServeHTTP(w, req) | ||||
| 		return | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func handleUIStub() http.Handler { | ||||
| 	stubHTML := ` | ||||
| 	<!DOCTYPE html> | ||||
| 	<html> | ||||
| 	<p>Vault UI is not available in this binary. To get Vault UI do one of the following:</p> | ||||
| 	<ul> | ||||
| 	<li><a href="https://www.vaultproject.io/downloads.html">Download an official release</a></li> | ||||
| 	<li>Run <code>make release</code> to create your own release binaries. | ||||
| 	<li>Run <code>make dev-ui</code> to create a development binary with the UI. | ||||
| 	</ul> | ||||
| 	</html> | ||||
| 	` | ||||
| 	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | ||||
| 		w.Write([]byte(stubHTML)) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func handleRootRedirect() http.Handler { | ||||
| 	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | ||||
| 		http.Redirect(w, req, "/ui/", 307) | ||||
| 		return | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| type UIAssetWrapper struct { | ||||
| 	FileSystem *assetfs.AssetFS | ||||
| } | ||||
|  | ||||
| func (fs *UIAssetWrapper) Open(name string) (http.File, error) { | ||||
| 	file, err := fs.FileSystem.Open(name) | ||||
| 	if err == nil { | ||||
| 		return file, nil | ||||
| 	} | ||||
| 	// serve index.html instead of 404ing | ||||
| 	if err == os.ErrNotExist { | ||||
| 		return fs.FileSystem.Open("index.html") | ||||
| 	} | ||||
| 	return nil, err | ||||
| } | ||||
|  | ||||
| func parseRequest(r *http.Request, w http.ResponseWriter, out interface{}) error { | ||||
| 	// Limit the maximum number of bytes to MaxRequestSize to protect | ||||
| 	// against an indefinite amount of data being read. | ||||
|   | ||||
							
								
								
									
										16
									
								
								http/stub_assets.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								http/stub_assets.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| // +build !ui | ||||
|  | ||||
| package http | ||||
|  | ||||
| import ( | ||||
| 	assetfs "github.com/elazarl/go-bindata-assetfs" | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	uiBuiltIn = false | ||||
| } | ||||
|  | ||||
| // assetFS is a stub for building Vault without a UI. | ||||
| func assetFS() *assetfs.AssetFS { | ||||
| 	return nil | ||||
| } | ||||
| @@ -363,8 +363,8 @@ type Core struct { | ||||
| 	replicationState           *uint32 | ||||
| 	activeNodeReplicationState *uint32 | ||||
|  | ||||
| 	// uiEnabled indicates whether Vault Web UI is enabled or not | ||||
| 	uiEnabled bool | ||||
| 	// uiConfig contains UI configuration | ||||
| 	uiConfig *UIConfig | ||||
|  | ||||
| 	// rawEnabled indicates whether the Raw endpoint is enabled | ||||
| 	rawEnabled bool | ||||
| @@ -620,6 +620,9 @@ func NewCore(conf *CoreConfig) (*Core, error) { | ||||
| 	} | ||||
| 	c.auditBackends = auditBackends | ||||
|  | ||||
| 	uiStoragePrefix := systemBarrierPrefix + "ui" | ||||
| 	c.uiConfig = NewUIConfig(conf.EnableUI, physical.NewView(c.physical, uiStoragePrefix), NewBarrierView(c.barrier, uiStoragePrefix)) | ||||
|  | ||||
| 	return c, nil | ||||
| } | ||||
|  | ||||
| @@ -1510,6 +1513,16 @@ func (c *Core) StepDown(req *logical.Request) (retErr error) { | ||||
| 	return retErr | ||||
| } | ||||
|  | ||||
| // UIEnabled returns if the UI is enabled | ||||
| func (c *Core) UIEnabled() bool { | ||||
| 	return c.uiConfig.Enabled() | ||||
| } | ||||
|  | ||||
| // UIHeaders returns configured UI headers | ||||
| func (c *Core) UIHeaders() (http.Header, error) { | ||||
| 	return c.uiConfig.Headers(context.Background()) | ||||
| } | ||||
|  | ||||
| // sealInternal is an internal method used to seal the vault.  It does not do | ||||
| // any authorization checking. The stateLock must be held prior to calling. | ||||
| func (c *Core) sealInternal(keepLock bool) error { | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"hash" | ||||
| 	"net/http" | ||||
| 	"path/filepath" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| @@ -77,6 +78,7 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend { | ||||
| 				"rotate", | ||||
| 				"config/cors", | ||||
| 				"config/auditing/*", | ||||
| 				"config/ui/headers/*", | ||||
| 				"plugins/catalog/*", | ||||
| 				"revoke-prefix/*", | ||||
| 				"revoke-force/*", | ||||
| @@ -148,6 +150,41 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend { | ||||
| 				HelpSynopsis:    strings.TrimSpace(sysHelp["config/cors"][1]), | ||||
| 			}, | ||||
|  | ||||
| 			&framework.Path{ | ||||
| 				Pattern: "config/ui/headers/" + framework.GenericNameRegex("header"), | ||||
|  | ||||
| 				Fields: map[string]*framework.FieldSchema{ | ||||
| 					"header": &framework.FieldSchema{ | ||||
| 						Type:        framework.TypeString, | ||||
| 						Description: "The name of the header.", | ||||
| 					}, | ||||
| 					"values": &framework.FieldSchema{ | ||||
| 						Type:        framework.TypeStringSlice, | ||||
| 						Description: "The values to set the header.", | ||||
| 					}, | ||||
| 				}, | ||||
|  | ||||
| 				Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 					logical.ReadOperation:   b.handleConfigUIHeadersRead, | ||||
| 					logical.UpdateOperation: b.handleConfigUIHeadersUpdate, | ||||
| 					logical.DeleteOperation: b.handleConfigUIHeadersDelete, | ||||
| 				}, | ||||
|  | ||||
| 				HelpDescription: strings.TrimSpace(sysHelp["config/ui/headers"][0]), | ||||
| 				HelpSynopsis:    strings.TrimSpace(sysHelp["config/ui/headers"][1]), | ||||
| 			}, | ||||
|  | ||||
| 			&framework.Path{ | ||||
| 				Pattern: "config/ui/headers/$", | ||||
|  | ||||
| 				Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 					logical.ListOperation: b.handleConfigUIHeadersList, | ||||
| 				}, | ||||
|  | ||||
| 				HelpDescription: strings.TrimSpace(sysHelp["config/ui/headers"][0]), | ||||
| 				HelpSynopsis:    strings.TrimSpace(sysHelp["config/ui/headers"][1]), | ||||
| 			}, | ||||
|  | ||||
| 			&framework.Path{ | ||||
| 				Pattern: "capabilities$", | ||||
|  | ||||
| @@ -2699,6 +2736,68 @@ func (b *SystemBackend) handleDisableAudit(ctx context.Context, req *logical.Req | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| func (b *SystemBackend) handleConfigUIHeadersRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	header := data.Get("header").(string) | ||||
|  | ||||
| 	value, err := b.Core.uiConfig.GetHeader(ctx, header) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if value == "" { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	return &logical.Response{ | ||||
| 		Data: map[string]interface{}{ | ||||
| 			"value": value, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (b *SystemBackend) handleConfigUIHeadersList(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	headers, err := b.Core.uiConfig.HeaderKeys(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if len(headers) == 0 { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	return logical.ListResponse(headers), nil | ||||
| } | ||||
|  | ||||
| func (b *SystemBackend) handleConfigUIHeadersUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	header := data.Get("header").(string) | ||||
| 	values := data.Get("values").([]string) | ||||
| 	if header == "" || len(values) == 0 { | ||||
| 		return logical.ErrorResponse("header and values must be specified"), logical.ErrInvalidRequest | ||||
| 	} | ||||
|  | ||||
| 	if strings.HasPrefix(strings.ToLower(header), "x-vault-") { | ||||
| 		return logical.ErrorResponse("X-Vault headers cannot be set"), logical.ErrInvalidRequest | ||||
| 	} | ||||
|  | ||||
| 	// Translate the list of values to the valid header string | ||||
| 	value := http.Header{ | ||||
| 		header: values, | ||||
| 	} | ||||
| 	err := b.Core.uiConfig.SetHeader(ctx, header, value.Get(header)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| func (b *SystemBackend) handleConfigUIHeadersDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	header := data.Get("header").(string) | ||||
| 	err := b.Core.uiConfig.DeleteHeader(ctx, header) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| // handleRawRead is used to read directly from the barrier | ||||
| func (b *SystemBackend) handleRawRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	path := data.Get("path").(string) | ||||
| @@ -3332,6 +3431,21 @@ This path responds to the following HTTP methods. | ||||
|         Clears the CORS configuration and disables acceptance of CORS requests. | ||||
| 		`, | ||||
| 	}, | ||||
| 	"config/ui/headers": { | ||||
| 		"Configures response headers that should be returned from the UI.", | ||||
| 		` | ||||
| This path responds to the following HTTP methods. | ||||
|     GET /<header> | ||||
|         Returns the header value. | ||||
|     POST /<header> | ||||
|         Sets the header value for the UI. | ||||
|     DELETE /<header> | ||||
|         Clears the header value for UI. | ||||
|          | ||||
|     LIST / | ||||
|         List the headers configured for the UI. | ||||
|         `, | ||||
| 	}, | ||||
| 	"init": { | ||||
| 		"Initializes or returns the initialization status of the Vault.", | ||||
| 		` | ||||
|   | ||||
							
								
								
									
										226
									
								
								vault/ui.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								vault/ui.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,226 @@ | ||||
| package vault | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| 	"github.com/hashicorp/vault/physical" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	uiConfigKey          = "config" | ||||
| 	uiConfigPlaintextKey = "config_plaintext" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	staticHeaders = http.Header{ | ||||
| 		"Content-Security-Policy": { | ||||
| 			"default-src 'none'; connect-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'unsafe-inline' 'self'; form-action 'none'; frame-ancestors 'none'", | ||||
| 		}, | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| // UIConfig contains UI configuration. This takes both a physical view and a barrier view | ||||
| // because it is stored in both plaintext and encrypted to allow for getting the header | ||||
| // values before the barrier is unsealed | ||||
| type UIConfig struct { | ||||
| 	l               sync.RWMutex | ||||
| 	physicalStorage physical.Backend | ||||
| 	barrierStorage  logical.Storage | ||||
|  | ||||
| 	enabled bool | ||||
| } | ||||
|  | ||||
| // NewUIConfig creates a new UI config | ||||
| func NewUIConfig(enabled bool, physicalStorage physical.Backend, barrierStorage logical.Storage) *UIConfig { | ||||
| 	return &UIConfig{ | ||||
| 		physicalStorage: physicalStorage, | ||||
| 		barrierStorage:  barrierStorage, | ||||
| 		enabled:         enabled, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Enabled returns if the UI is enabled | ||||
| func (c *UIConfig) Enabled() bool { | ||||
| 	c.l.RLock() | ||||
| 	defer c.l.RUnlock() | ||||
| 	return c.enabled | ||||
| } | ||||
|  | ||||
| // Headers returns the response headers that should be returned in the UI | ||||
| func (c *UIConfig) Headers(ctx context.Context) (http.Header, error) { | ||||
| 	c.l.RLock() | ||||
| 	defer c.l.RUnlock() | ||||
|  | ||||
| 	config, err := c.get(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	headers := make(http.Header) | ||||
| 	if config != nil { | ||||
| 		headers = config.Headers | ||||
| 	} | ||||
|  | ||||
| 	for k := range staticHeaders { | ||||
| 		v := staticHeaders.Get(k) | ||||
| 		headers.Set(k, v) | ||||
| 	} | ||||
| 	return headers, nil | ||||
| } | ||||
|  | ||||
| // HeaderKeys returns the list of the configured headers | ||||
| func (c *UIConfig) HeaderKeys(ctx context.Context) ([]string, error) { | ||||
| 	c.l.RLock() | ||||
| 	defer c.l.RUnlock() | ||||
|  | ||||
| 	config, err := c.get(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if config == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	var keys []string | ||||
| 	for k := range config.Headers { | ||||
| 		keys = append(keys, k) | ||||
| 	} | ||||
| 	return keys, nil | ||||
| } | ||||
|  | ||||
| // GetHeader retrieves the configured value for the given header | ||||
| func (c *UIConfig) GetHeader(ctx context.Context, header string) (string, error) { | ||||
| 	c.l.RLock() | ||||
| 	defer c.l.RUnlock() | ||||
|  | ||||
| 	config, err := c.get(ctx) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	if config == nil { | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	value := config.Headers.Get(header) | ||||
| 	return value, nil | ||||
| } | ||||
|  | ||||
| // SetHeader sets the value for the given header | ||||
| func (c *UIConfig) SetHeader(ctx context.Context, header, value string) error { | ||||
| 	if val := staticHeaders.Get(header); val != "" { | ||||
| 		return fmt.Errorf("the header %s is not settable", header) | ||||
| 	} | ||||
|  | ||||
| 	c.l.Lock() | ||||
| 	defer c.l.Unlock() | ||||
|  | ||||
| 	config, err := c.get(ctx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if config == nil { | ||||
| 		config = &uiConfigEntry{ | ||||
| 			Headers: http.Header{ | ||||
| 				header: {value}, | ||||
| 			}, | ||||
| 		} | ||||
| 	} else { | ||||
| 		config.Headers.Set(header, value) | ||||
| 	} | ||||
| 	return c.save(ctx, config) | ||||
| } | ||||
|  | ||||
| // DeleteHeader deletes the header configuration for the given header | ||||
| func (c *UIConfig) DeleteHeader(ctx context.Context, header string) error { | ||||
| 	c.l.Lock() | ||||
| 	defer c.l.Unlock() | ||||
|  | ||||
| 	config, err := c.get(ctx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if config == nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	config.Headers.Del(header) | ||||
| 	return c.save(ctx, config) | ||||
| } | ||||
|  | ||||
| func (c *UIConfig) get(ctx context.Context) (*uiConfigEntry, error) { | ||||
| 	// Read plaintext always to ensure in sync with barrier value | ||||
| 	plaintextConfigRaw, err := c.physicalStorage.Get(ctx, uiConfigPlaintextKey) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	configRaw, err := c.barrierStorage.Get(ctx, uiConfigKey) | ||||
| 	if err == nil { | ||||
| 		if configRaw == nil { | ||||
| 			return nil, nil | ||||
| 		} | ||||
| 		config := new(uiConfigEntry) | ||||
| 		if err := json.Unmarshal(configRaw.Value, config); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		// Check that plaintext value matches barrier value, if not sync values | ||||
| 		if plaintextConfigRaw == nil || bytes.Compare(plaintextConfigRaw.Value, configRaw.Value) != 0 { | ||||
| 			if err := c.save(ctx, config); err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 		} | ||||
| 		return config, nil | ||||
| 	} | ||||
|  | ||||
| 	// Respond with error if not sealed | ||||
| 	if !strings.Contains(err.Error(), ErrBarrierSealed.Error()) { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Respond with plaintext value | ||||
| 	if configRaw == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	config := new(uiConfigEntry) | ||||
| 	if err := json.Unmarshal(plaintextConfigRaw.Value, config); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return config, nil | ||||
| } | ||||
|  | ||||
| func (c *UIConfig) save(ctx context.Context, config *uiConfigEntry) error { | ||||
| 	if len(config.Headers) == 0 { | ||||
| 		if err := c.physicalStorage.Delete(ctx, uiConfigPlaintextKey); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		return c.barrierStorage.Delete(ctx, uiConfigKey) | ||||
| 	} | ||||
|  | ||||
| 	configRaw, err := json.Marshal(config) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	entry := &physical.Entry{ | ||||
| 		Key:   uiConfigPlaintextKey, | ||||
| 		Value: configRaw, | ||||
| 	} | ||||
| 	if err := c.physicalStorage.Put(ctx, entry); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	barrEntry := &logical.StorageEntry{ | ||||
| 		Key:   uiConfigKey, | ||||
| 		Value: configRaw, | ||||
| 	} | ||||
| 	return c.barrierStorage.Put(ctx, barrEntry) | ||||
| } | ||||
|  | ||||
| type uiConfigEntry struct { | ||||
| 	Headers http.Header `json:"headers"` | ||||
| } | ||||
							
								
								
									
										125
									
								
								vault/ui_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								vault/ui_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | ||||
| package vault | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/helper/logformat" | ||||
| 	"github.com/hashicorp/vault/physical/inmem" | ||||
| 	log "github.com/mgutz/logxi/v1" | ||||
| ) | ||||
|  | ||||
| func TestConfig_Enabled(t *testing.T) { | ||||
| 	logger := logformat.NewVaultLogger(log.LevelTrace) | ||||
| 	phys, err := inmem.NewTransactionalInmem(nil, logger) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	logl := &logical.InmemStorage{} | ||||
|  | ||||
| 	config := NewUIConfig(true, phys, logl) | ||||
| 	if !config.Enabled() { | ||||
| 		t.Fatal("ui should be enabled") | ||||
| 	} | ||||
|  | ||||
| 	config = NewUIConfig(false, phys, logl) | ||||
| 	if config.Enabled() { | ||||
| 		t.Fatal("ui should not be enabled") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestConfig_Headers(t *testing.T) { | ||||
| 	logger := logformat.NewVaultLogger(log.LevelTrace) | ||||
| 	phys, err := inmem.NewTransactionalInmem(nil, logger) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	logl := &logical.InmemStorage{} | ||||
|  | ||||
| 	config := NewUIConfig(true, phys, logl) | ||||
| 	headers, err := config.Headers(context.Background()) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %v", err) | ||||
| 	} | ||||
| 	if len(headers) != len(staticHeaders) { | ||||
| 		t.Fatalf("expected %d headers, got %d", len(staticHeaders), len(headers)) | ||||
| 	} | ||||
|  | ||||
| 	head, err := config.GetHeader(context.Background(), "Test-Header") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %v", err) | ||||
| 	} | ||||
| 	if head != "" { | ||||
| 		t.Fatal("header returned found, should not be found") | ||||
| 	} | ||||
| 	err = config.SetHeader(context.Background(), "Test-Header", "123") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %v", err) | ||||
| 	} | ||||
| 	head, err = config.GetHeader(context.Background(), "Test-Header") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %v", err) | ||||
| 	} | ||||
| 	if head == "" { | ||||
| 		t.Fatal("header not found when it should be") | ||||
| 	} | ||||
| 	if head != "123" { | ||||
| 		t.Fatalf("expected: %s, got: %s", "123", head) | ||||
| 	} | ||||
|  | ||||
| 	head, err = config.GetHeader(context.Background(), "tEST-hEADER") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %v", err) | ||||
| 	} | ||||
| 	if head == "" { | ||||
| 		t.Fatal("header not found when it should be") | ||||
| 	} | ||||
| 	if head != "123" { | ||||
| 		t.Fatalf("expected: %s, got: %s", "123", head) | ||||
| 	} | ||||
|  | ||||
| 	keys, err := config.HeaderKeys(context.Background()) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %v", err) | ||||
| 	} | ||||
| 	if len(keys) != 1 { | ||||
| 		t.Fatalf("expected 1 key, got %d", len(keys)) | ||||
| 	} | ||||
|  | ||||
| 	err = config.SetHeader(context.Background(), "Test-Header-2", "321") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %v", err) | ||||
| 	} | ||||
| 	keys, err = config.HeaderKeys(context.Background()) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %v", err) | ||||
| 	} | ||||
| 	if len(keys) != 2 { | ||||
| 		t.Fatalf("expected 1 key, got %d", len(keys)) | ||||
| 	} | ||||
| 	err = config.DeleteHeader(context.Background(), "Test-Header-2") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	err = config.DeleteHeader(context.Background(), "Test-Header") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %v", err) | ||||
| 	} | ||||
| 	head, err = config.GetHeader(context.Background(), "Test-Header") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %v", err) | ||||
| 	} | ||||
| 	if head != "" { | ||||
| 		t.Fatal("header returned found, should not be found") | ||||
| 	} | ||||
| 	keys, err = config.HeaderKeys(context.Background()) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %v", err) | ||||
| 	} | ||||
| 	if len(keys) != 0 { | ||||
| 		t.Fatalf("expected 0 key, got %d", len(keys)) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										23
									
								
								vendor/github.com/elazarl/go-bindata-assetfs/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								vendor/github.com/elazarl/go-bindata-assetfs/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| Copyright (c) 2014, Elazar Leibovich | ||||
| All rights reserved. | ||||
|  | ||||
| Redistribution and use in source and binary forms, with or without | ||||
| modification, are permitted provided that the following conditions are met: | ||||
|  | ||||
| * Redistributions of source code must retain the above copyright notice, this | ||||
|   list of conditions and the following disclaimer. | ||||
|  | ||||
| * Redistributions in binary form must reproduce the above copyright notice, | ||||
|   this list of conditions and the following disclaimer in the documentation | ||||
|   and/or other materials provided with the distribution. | ||||
|  | ||||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||||
| AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||||
| IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||
| DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||||
| FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||||
| DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||||
| SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||||
| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||||
| OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
							
								
								
									
										46
									
								
								vendor/github.com/elazarl/go-bindata-assetfs/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								vendor/github.com/elazarl/go-bindata-assetfs/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| # go-bindata-assetfs | ||||
|  | ||||
| Serve embedded files from [jteeuwen/go-bindata](https://github.com/jteeuwen/go-bindata) with `net/http`. | ||||
|  | ||||
| [GoDoc](http://godoc.org/github.com/elazarl/go-bindata-assetfs) | ||||
|  | ||||
| ### Installation | ||||
|  | ||||
| Install with | ||||
|  | ||||
|     $ go get github.com/jteeuwen/go-bindata/... | ||||
|     $ go get github.com/elazarl/go-bindata-assetfs/... | ||||
|  | ||||
| ### Creating embedded data | ||||
|  | ||||
| Usage is identical to [jteeuwen/go-bindata](https://github.com/jteeuwen/go-bindata) usage, | ||||
| instead of running `go-bindata` run `go-bindata-assetfs`. | ||||
|  | ||||
| The tool will create a `bindata_assetfs.go` file, which contains the embedded data. | ||||
|  | ||||
| A typical use case is | ||||
|  | ||||
|     $ go-bindata-assetfs data/... | ||||
|  | ||||
| ### Using assetFS in your code | ||||
|  | ||||
| The generated file provides an `assetFS()` function that returns a `http.Filesystem` | ||||
| wrapping the embedded files. What you usually want to do is: | ||||
|  | ||||
|     http.Handle("/", http.FileServer(assetFS())) | ||||
|  | ||||
| This would run an HTTP server serving the embedded files. | ||||
|  | ||||
| ## Without running binary tool | ||||
|  | ||||
| You can always just run the `go-bindata` tool, and then | ||||
|  | ||||
| use | ||||
|  | ||||
|      import "github.com/elazarl/go-bindata-assetfs" | ||||
|      ... | ||||
|      http.Handle("/", | ||||
|         http.FileServer( | ||||
|         &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo, Prefix: "data"})) | ||||
|  | ||||
| to serve files embedded from the `data` directory. | ||||
							
								
								
									
										167
									
								
								vendor/github.com/elazarl/go-bindata-assetfs/assetfs.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								vendor/github.com/elazarl/go-bindata-assetfs/assetfs.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | ||||
| package assetfs | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	defaultFileTimestamp = time.Now() | ||||
| ) | ||||
|  | ||||
| // FakeFile implements os.FileInfo interface for a given path and size | ||||
| type FakeFile struct { | ||||
| 	// Path is the path of this file | ||||
| 	Path string | ||||
| 	// Dir marks of the path is a directory | ||||
| 	Dir bool | ||||
| 	// Len is the length of the fake file, zero if it is a directory | ||||
| 	Len int64 | ||||
| 	// Timestamp is the ModTime of this file | ||||
| 	Timestamp time.Time | ||||
| } | ||||
|  | ||||
| func (f *FakeFile) Name() string { | ||||
| 	_, name := filepath.Split(f.Path) | ||||
| 	return name | ||||
| } | ||||
|  | ||||
| func (f *FakeFile) Mode() os.FileMode { | ||||
| 	mode := os.FileMode(0644) | ||||
| 	if f.Dir { | ||||
| 		return mode | os.ModeDir | ||||
| 	} | ||||
| 	return mode | ||||
| } | ||||
|  | ||||
| func (f *FakeFile) ModTime() time.Time { | ||||
| 	return f.Timestamp | ||||
| } | ||||
|  | ||||
| func (f *FakeFile) Size() int64 { | ||||
| 	return f.Len | ||||
| } | ||||
|  | ||||
| func (f *FakeFile) IsDir() bool { | ||||
| 	return f.Mode().IsDir() | ||||
| } | ||||
|  | ||||
| func (f *FakeFile) Sys() interface{} { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // AssetFile implements http.File interface for a no-directory file with content | ||||
| type AssetFile struct { | ||||
| 	*bytes.Reader | ||||
| 	io.Closer | ||||
| 	FakeFile | ||||
| } | ||||
|  | ||||
| func NewAssetFile(name string, content []byte, timestamp time.Time) *AssetFile { | ||||
| 	if timestamp.IsZero() { | ||||
| 		timestamp = defaultFileTimestamp | ||||
| 	} | ||||
| 	return &AssetFile{ | ||||
| 		bytes.NewReader(content), | ||||
| 		ioutil.NopCloser(nil), | ||||
| 		FakeFile{name, false, int64(len(content)), timestamp}} | ||||
| } | ||||
|  | ||||
| func (f *AssetFile) Readdir(count int) ([]os.FileInfo, error) { | ||||
| 	return nil, errors.New("not a directory") | ||||
| } | ||||
|  | ||||
| func (f *AssetFile) Size() int64 { | ||||
| 	return f.FakeFile.Size() | ||||
| } | ||||
|  | ||||
| func (f *AssetFile) Stat() (os.FileInfo, error) { | ||||
| 	return f, nil | ||||
| } | ||||
|  | ||||
| // AssetDirectory implements http.File interface for a directory | ||||
| type AssetDirectory struct { | ||||
| 	AssetFile | ||||
| 	ChildrenRead int | ||||
| 	Children     []os.FileInfo | ||||
| } | ||||
|  | ||||
| func NewAssetDirectory(name string, children []string, fs *AssetFS) *AssetDirectory { | ||||
| 	fileinfos := make([]os.FileInfo, 0, len(children)) | ||||
| 	for _, child := range children { | ||||
| 		_, err := fs.AssetDir(filepath.Join(name, child)) | ||||
| 		fileinfos = append(fileinfos, &FakeFile{child, err == nil, 0, time.Time{}}) | ||||
| 	} | ||||
| 	return &AssetDirectory{ | ||||
| 		AssetFile{ | ||||
| 			bytes.NewReader(nil), | ||||
| 			ioutil.NopCloser(nil), | ||||
| 			FakeFile{name, true, 0, time.Time{}}, | ||||
| 		}, | ||||
| 		0, | ||||
| 		fileinfos} | ||||
| } | ||||
|  | ||||
| func (f *AssetDirectory) Readdir(count int) ([]os.FileInfo, error) { | ||||
| 	if count <= 0 { | ||||
| 		return f.Children, nil | ||||
| 	} | ||||
| 	if f.ChildrenRead+count > len(f.Children) { | ||||
| 		count = len(f.Children) - f.ChildrenRead | ||||
| 	} | ||||
| 	rv := f.Children[f.ChildrenRead : f.ChildrenRead+count] | ||||
| 	f.ChildrenRead += count | ||||
| 	return rv, nil | ||||
| } | ||||
|  | ||||
| func (f *AssetDirectory) Stat() (os.FileInfo, error) { | ||||
| 	return f, nil | ||||
| } | ||||
|  | ||||
| // AssetFS implements http.FileSystem, allowing | ||||
| // embedded files to be served from net/http package. | ||||
| type AssetFS struct { | ||||
| 	// Asset should return content of file in path if exists | ||||
| 	Asset func(path string) ([]byte, error) | ||||
| 	// AssetDir should return list of files in the path | ||||
| 	AssetDir func(path string) ([]string, error) | ||||
| 	// AssetInfo should return the info of file in path if exists | ||||
| 	AssetInfo func(path string) (os.FileInfo, error) | ||||
| 	// Prefix would be prepended to http requests | ||||
| 	Prefix string | ||||
| } | ||||
|  | ||||
| func (fs *AssetFS) Open(name string) (http.File, error) { | ||||
| 	name = path.Join(fs.Prefix, name) | ||||
| 	if len(name) > 0 && name[0] == '/' { | ||||
| 		name = name[1:] | ||||
| 	} | ||||
| 	if b, err := fs.Asset(name); err == nil { | ||||
| 		timestamp := defaultFileTimestamp | ||||
| 		if fs.AssetInfo != nil { | ||||
| 			if info, err := fs.AssetInfo(name); err == nil { | ||||
| 				timestamp = info.ModTime() | ||||
| 			} | ||||
| 		} | ||||
| 		return NewAssetFile(name, b, timestamp), nil | ||||
| 	} | ||||
| 	if children, err := fs.AssetDir(name); err == nil { | ||||
| 		return NewAssetDirectory(name, children, fs), nil | ||||
| 	} else { | ||||
| 		// If the error is not found, return an error that will | ||||
| 		// result in a 404 error. Otherwise the server returns | ||||
| 		// a 500 error for files not found. | ||||
| 		if strings.Contains(err.Error(), "not found") { | ||||
| 			return nil, os.ErrNotExist | ||||
| 		} | ||||
| 		return nil, err | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										13
									
								
								vendor/github.com/elazarl/go-bindata-assetfs/doc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								vendor/github.com/elazarl/go-bindata-assetfs/doc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| // assetfs allows packages to serve static content embedded | ||||
| // with the go-bindata tool with the standard net/http package. | ||||
| // | ||||
| // See https://github.com/jteeuwen/go-bindata for more information | ||||
| // about embedding binary data with go-bindata. | ||||
| // | ||||
| // Usage example, after running | ||||
| //    $ go-bindata data/... | ||||
| // use: | ||||
| //     http.Handle("/", | ||||
| //        http.FileServer( | ||||
| //        &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, Prefix: "data"})) | ||||
| package assetfs | ||||
							
								
								
									
										6
									
								
								vendor/vendor.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								vendor/vendor.json
									
									
									
									
										vendored
									
									
								
							| @@ -864,6 +864,12 @@ | ||||
| 			"revision": "bb3d318650d48840a39aa21a027c6630e198e626", | ||||
| 			"revisionTime": "2017-11-10T20:55:13Z" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"checksumSHA1": "7DxViusFRJ7UPH0jZqYatwDrOkY=", | ||||
| 			"path": "github.com/elazarl/go-bindata-assetfs", | ||||
| 			"revision": "38087fe4dafb822e541b3f7955075cc1c30bd294", | ||||
| 			"revisionTime": "2018-02-23T16:03:09Z" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"checksumSHA1": "5BP5xofo0GoFi6FtgqFFbmHyUKI=", | ||||
| 			"path": "github.com/fatih/structs", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user