mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 18:17:55 +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:
committed by
Matthew Irish
parent
2c2f0d853f
commit
af33ece136
1
.gitignore
vendored
1
.gitignore
vendored
@@ -65,6 +65,7 @@ tags
|
|||||||
ui/dist
|
ui/dist
|
||||||
ui/tmp
|
ui/tmp
|
||||||
ui/root
|
ui/root
|
||||||
|
http/bindata_assetfs.go
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
ui/node_modules
|
ui/node_modules
|
||||||
|
|||||||
@@ -452,6 +452,7 @@ func (c *ServerCommand) Run(args []string) int {
|
|||||||
ClusterName: config.ClusterName,
|
ClusterName: config.ClusterName,
|
||||||
CacheSize: config.CacheSize,
|
CacheSize: config.CacheSize,
|
||||||
PluginDirectory: config.PluginDirectory,
|
PluginDirectory: config.PluginDirectory,
|
||||||
|
EnableUI: config.EnableUI,
|
||||||
EnableRaw: config.EnableRawEndpoint,
|
EnableRaw: config.EnableRawEndpoint,
|
||||||
}
|
}
|
||||||
if c.flagDev {
|
if c.flagDev {
|
||||||
@@ -607,6 +608,16 @@ CLUSTER_SYNTHESIS_COMPLETE:
|
|||||||
coreConfig.ClusterAddr = u.String()
|
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
|
// Initialize the core
|
||||||
core, newCoreError := vault.NewCore(coreConfig)
|
core, newCoreError := vault.NewCore(coreConfig)
|
||||||
if newCoreError != nil {
|
if newCoreError != nil {
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/elazarl/go-bindata-assetfs"
|
||||||
"github.com/hashicorp/errwrap"
|
"github.com/hashicorp/errwrap"
|
||||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||||
"github.com/hashicorp/vault/helper/consts"
|
"github.com/hashicorp/vault/helper/consts"
|
||||||
@@ -54,6 +56,9 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
ReplicationStaleReadTimeout = 2 * time.Second
|
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
|
// 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/sys/", handleRequestForwarding(core, handleLogical(core, false, nil)))
|
||||||
mux.Handle("/v1/", 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.
|
// Wrap the handler in another handler to trigger all help paths.
|
||||||
helpWrappedHandler := wrapHelpHandler(mux, core)
|
helpWrappedHandler := wrapHelpHandler(mux, core)
|
||||||
@@ -145,6 +158,72 @@ func stripPrefix(prefix, path string) (string, bool) {
|
|||||||
return path, true
|
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 {
|
func parseRequest(r *http.Request, w http.ResponseWriter, out interface{}) error {
|
||||||
// Limit the maximum number of bytes to MaxRequestSize to protect
|
// Limit the maximum number of bytes to MaxRequestSize to protect
|
||||||
// against an indefinite amount of data being read.
|
// 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
|
replicationState *uint32
|
||||||
activeNodeReplicationState *uint32
|
activeNodeReplicationState *uint32
|
||||||
|
|
||||||
// uiEnabled indicates whether Vault Web UI is enabled or not
|
// uiConfig contains UI configuration
|
||||||
uiEnabled bool
|
uiConfig *UIConfig
|
||||||
|
|
||||||
// rawEnabled indicates whether the Raw endpoint is enabled
|
// rawEnabled indicates whether the Raw endpoint is enabled
|
||||||
rawEnabled bool
|
rawEnabled bool
|
||||||
@@ -620,6 +620,9 @@ func NewCore(conf *CoreConfig) (*Core, error) {
|
|||||||
}
|
}
|
||||||
c.auditBackends = auditBackends
|
c.auditBackends = auditBackends
|
||||||
|
|
||||||
|
uiStoragePrefix := systemBarrierPrefix + "ui"
|
||||||
|
c.uiConfig = NewUIConfig(conf.EnableUI, physical.NewView(c.physical, uiStoragePrefix), NewBarrierView(c.barrier, uiStoragePrefix))
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1510,6 +1513,16 @@ func (c *Core) StepDown(req *logical.Request) (retErr error) {
|
|||||||
return retErr
|
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
|
// 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.
|
// any authorization checking. The stateLock must be held prior to calling.
|
||||||
func (c *Core) sealInternal(keepLock bool) error {
|
func (c *Core) sealInternal(keepLock bool) error {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -77,6 +78,7 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend {
|
|||||||
"rotate",
|
"rotate",
|
||||||
"config/cors",
|
"config/cors",
|
||||||
"config/auditing/*",
|
"config/auditing/*",
|
||||||
|
"config/ui/headers/*",
|
||||||
"plugins/catalog/*",
|
"plugins/catalog/*",
|
||||||
"revoke-prefix/*",
|
"revoke-prefix/*",
|
||||||
"revoke-force/*",
|
"revoke-force/*",
|
||||||
@@ -148,6 +150,41 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend {
|
|||||||
HelpSynopsis: strings.TrimSpace(sysHelp["config/cors"][1]),
|
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{
|
&framework.Path{
|
||||||
Pattern: "capabilities$",
|
Pattern: "capabilities$",
|
||||||
|
|
||||||
@@ -2699,6 +2736,68 @@ func (b *SystemBackend) handleDisableAudit(ctx context.Context, req *logical.Req
|
|||||||
return nil, nil
|
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
|
// 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) {
|
func (b *SystemBackend) handleRawRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
path := data.Get("path").(string)
|
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.
|
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": {
|
"init": {
|
||||||
"Initializes or returns the initialization status of the Vault.",
|
"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",
|
"revision": "bb3d318650d48840a39aa21a027c6630e198e626",
|
||||||
"revisionTime": "2017-11-10T20:55:13Z"
|
"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=",
|
"checksumSHA1": "5BP5xofo0GoFi6FtgqFFbmHyUKI=",
|
||||||
"path": "github.com/fatih/structs",
|
"path": "github.com/fatih/structs",
|
||||||
|
|||||||
Reference in New Issue
Block a user