diff --git a/builtin/logical/transit/path_restore.go b/builtin/logical/transit/path_restore.go index f619d162588b..a8483c1554ec 100644 --- a/builtin/logical/transit/path_restore.go +++ b/builtin/logical/transit/path_restore.go @@ -2,6 +2,8 @@ package transit import ( "context" + "errors" + "strings" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" @@ -42,8 +44,19 @@ func (b *backend) pathRestoreUpdate(ctx context.Context, req *logical.Request, d return logical.ErrorResponse("'backup' must be supplied"), nil } - return nil, b.lm.RestorePolicy(ctx, req.Storage, d.Get("name").(string), backupB64, force) + keyName := d.Get("name").(string) + // if a name is given, make sure it does not contain any slashes and look like + // a path + if keyName != "" { + if strings.Contains(keyName, "/") { + return nil, ErrInvalidKeyName + } + } + + return nil, b.lm.RestorePolicy(ctx, req.Storage, keyName, backupB64, force) } const pathRestoreHelpSyn = `Restore the named key` const pathRestoreHelpDesc = `This path is used to restore the named key.` + +var ErrInvalidKeyName = errors.New("key names cannot be paths") diff --git a/builtin/logical/transit/path_restore_test.go b/builtin/logical/transit/path_restore_test.go index 5278d144ed99..c4c4583882e7 100644 --- a/builtin/logical/transit/path_restore_test.go +++ b/builtin/logical/transit/path_restore_test.go @@ -94,6 +94,9 @@ func TestTransit_Restore(t *testing.T) { Force *bool // The error we expect, if any ExpectedErr error + + // RestoreName is used to restore the key to a differnt name + RestoreName string }{ { // key does not already exist @@ -116,6 +119,33 @@ func TestTransit_Restore(t *testing.T) { Name: "Restore-with-force-no-seed", Force: boolPtr(true), }, + { + // key already exists, restore to new name + Name: "Restore-new-name", + Seed: true, + RestoreName: "new-key", + }, + { + // key already exists, restore to bad path, should error + Name: "Restore-new-name-bad-path", + Seed: true, + RestoreName: "sub/path/new-key", + ExpectedErr: ErrInvalidKeyName, + }, + { + // using force shouldn't matter if the restore key name is different + Name: "Restore-with-force-seed-new-name", + Seed: true, + Force: boolPtr(true), + RestoreName: "other-key", + }, + { + // using force shouldn't matter if the restore key name is different + Name: "Restore-with-out-force-seed-new-name", + Seed: true, + Force: boolPtr(false), + RestoreName: "other-key", + }, { // using force shouldn't matter if the key doesn't exist Name: "Restore-force-false", @@ -154,8 +184,13 @@ func TestTransit_Restore(t *testing.T) { } } + restorePath := "restore" + if tc.RestoreName != "" { + restorePath = fmt.Sprintf("%s/%s", restorePath, tc.RestoreName) + } + restoreReq := &logical.Request{ - Path: "restore", + Path: restorePath, Operation: logical.UpdateOperation, Storage: s, Data: map[string]interface{}{ @@ -184,12 +219,42 @@ func TestTransit_Restore(t *testing.T) { } } + readKeyName := keyName + if tc.RestoreName != "" { + readKeyName = tc.RestoreName + } + + // read the key and make sure it's there + readReq := &logical.Request{ + Path: "keys/" + readKeyName, + Operation: logical.ReadOperation, + Storage: s, + } + + resp, err = b.HandleRequest(context.Background(), readReq) + if resp != nil && resp.IsError() { + t.Fatalf("resp: %#v\nerr: %v", resp, err) + } + + if tc.ExpectedErr == nil && resp == nil { + t.Fatal("expected to find a key, but got none") + } + // cleanup / delete key after each run keyReq.Operation = logical.DeleteOperation resp, err = b.HandleRequest(context.Background(), keyReq) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("resp: %#v\nerr: %v", resp, err) } + + // cleanup / delete restore key after each run, if it was created + if tc.RestoreName != "" && tc.ExpectedErr == nil { + readReq.Operation = logical.DeleteOperation + resp, err = b.HandleRequest(context.Background(), readReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("resp: %#v\nerr: %v", resp, err) + } + } }) } } diff --git a/command/server.go b/command/server.go index 8a267a08b87e..10f93b50758b 100644 --- a/command/server.go +++ b/command/server.go @@ -2270,6 +2270,7 @@ func (c *ServerCommand) setupTelemetry(config *server.Config) (*metricsutil.Metr metricsConf := metrics.DefaultConfig("vault") metricsConf.EnableHostname = !telConfig.DisableHostname + metricsConf.EnableHostnameLabel = telConfig.EnableHostnameLabel // Configure the statsite sink var fanout metrics.FanoutSink diff --git a/command/server/config.go b/command/server/config.go index d558019adf25..41f9150f2bce 100644 --- a/command/server/config.go +++ b/command/server/config.go @@ -166,7 +166,8 @@ type Telemetry struct { StatsiteAddr string `hcl:"statsite_address"` StatsdAddr string `hcl:"statsd_address"` - DisableHostname bool `hcl:"disable_hostname"` + DisableHostname bool `hcl:"disable_hostname"` + EnableHostnameLabel bool `hcl:"enable_hostname_label"` // Circonus: see https://github.com/circonus-labs/circonus-gometrics // for more details on the various configuration options. diff --git a/helper/metricsutil/metricsutil.go b/helper/metricsutil/metricsutil.go index f6ac99f89239..83ca85a2d8fe 100644 --- a/helper/metricsutil/metricsutil.go +++ b/helper/metricsutil/metricsutil.go @@ -16,6 +16,8 @@ import ( const ( OpenMetricsMIMEType = "application/openmetrics-text" + PrometheusSchemaMIMEType = "prometheus/telemetry" + // ErrorContentType is the content type returned by an error response. ErrorContentType = "text/plain" ) @@ -38,7 +40,14 @@ func FormatFromRequest(req *logical.Request) string { if len(acceptHeaders) > 0 { acceptHeader := acceptHeaders[0] if strings.HasPrefix(acceptHeader, OpenMetricsMIMEType) { - return "prometheus" + return PrometheusMetricFormat + } + + // Look for prometheus accept header + for _, header := range acceptHeaders { + if strings.Contains(header, PrometheusSchemaMIMEType) { + return PrometheusMetricFormat + } } } return "" diff --git a/helper/metricsutil/metricsutil_test.go b/helper/metricsutil/metricsutil_test.go new file mode 100644 index 000000000000..bf33b76045f9 --- /dev/null +++ b/helper/metricsutil/metricsutil_test.go @@ -0,0 +1,45 @@ +package metricsutil + +import ( + "github.com/hashicorp/vault/sdk/logical" + "testing" +) + +func TestFormatFromRequest(t *testing.T) { + testCases := []struct { + original *logical.Request + expected string + }{ + { + original: &logical.Request{Headers: map[string][]string{ + "Accept": { + "application/vnd.google.protobuf", + "schema=\"prometheus/telemetry\"", + }, + }}, + expected: "prometheus", + }, + { + original: &logical.Request{Headers: map[string][]string{ + "Accept": { + "schema=\"prometheus\"", + }, + }}, + expected: "", + }, + { + original: &logical.Request{Headers: map[string][]string{ + "Accept": { + "application/openmetrics-text", + }, + }}, + expected: "prometheus", + }, + } + + for _, tCase := range testCases { + if metricsType := FormatFromRequest(tCase.original); metricsType != tCase.expected { + t.Fatalf("expected %s but got %s", tCase.expected, metricsType) + } + } +} diff --git a/physical/s3/s3.go b/physical/s3/s3.go index 315b6d326965..b131678d47b7 100644 --- a/physical/s3/s3.go +++ b/physical/s3/s3.go @@ -225,6 +225,11 @@ func (s *S3Backend) Get(ctx context.Context, key string) (*physical.Entry, error return nil, err } + // Strip path prefix + if s.path != "" { + key = strings.TrimPrefix(key, s.path+"/") + } + ent := &physical.Entry{ Key: key, Value: data.Bytes(), @@ -266,8 +271,8 @@ func (s *S3Backend) List(ctx context.Context, prefix string) ([]string, error) { // Setup prefix prefix = path.Join(s.path, prefix) - // Validate prefix is ending with a "/" - if !strings.HasSuffix(prefix, "/") { + // Validate prefix (if present) is ending with a "/" + if prefix != "" && !strings.HasSuffix(prefix, "/") { prefix += "/" } diff --git a/physical/s3/s3_test.go b/physical/s3/s3_test.go index 849ab6b27857..a7a724554190 100644 --- a/physical/s3/s3_test.go +++ b/physical/s3/s3_test.go @@ -34,12 +34,12 @@ func DoS3BackendTest(t *testing.T, kmsKeyId string) { credsChain, err := credsConfig.GenerateCredentialChain() if err != nil { - t.SkipNow() + t.Fatal(err) } _, err = credsChain.Get() if err != nil { - t.SkipNow() + t.Fatal(err) } // If the variable is empty or doesn't exist, the default diff --git a/sdk/framework/lease.go b/sdk/framework/lease.go index c79f3cbbabf7..f5c68b841d0f 100644 --- a/sdk/framework/lease.go +++ b/sdk/framework/lease.go @@ -3,6 +3,7 @@ package framework import ( "context" "fmt" + "strings" "time" "github.com/hashicorp/vault/sdk/logical" @@ -61,7 +62,8 @@ func CalculateTTL(sysView logical.SystemView, increment, backendTTL, period, bac // Cap the period value to the sys max_ttl value if period > maxTTL { warnings = append(warnings, - fmt.Sprintf("period of %q exceeded the effective max_ttl of %q; period value is capped accordingly", period, maxTTL)) + fmt.Sprintf("period of %q exceeded the effective max_ttl of %q; period value is capped accordingly", + humanDuration(period), humanDuration(maxTTL))) period = maxTTL } ttl = period @@ -97,10 +99,27 @@ func CalculateTTL(sysView logical.SystemView, increment, backendTTL, period, bac // cap the increment to whatever is left if maxValidTTL-ttl < 0 { warnings = append(warnings, - fmt.Sprintf("TTL of %q exceeded the effective max_ttl of %q; TTL value is capped accordingly", ttl, maxValidTTL)) + fmt.Sprintf("TTL of %q exceeded the effective max_ttl of %q; TTL value is capped accordingly", + humanDuration(ttl), humanDuration(maxValidTTL))) ttl = maxValidTTL } } return ttl, warnings, nil } + +// humanDuration prints the time duration without zero elements. +func humanDuration(d time.Duration) string { + if d == 0 { + return "0s" + } + + s := d.String() + if strings.HasSuffix(s, "m0s") { + s = s[:len(s)-2] + } + if idx := strings.Index(s, "h0m"); idx > 0 { + s = s[:idx+1] + s[idx+3:] + } + return s +} diff --git a/ui/app/models/cluster.js b/ui/app/models/cluster.js index db24b159ef8b..f7542c1ce2e9 100644 --- a/ui/app/models/cluster.js +++ b/ui/app/models/cluster.js @@ -72,7 +72,7 @@ export default DS.Model.extend({ }), stateGlyph(state) { - const glyph = 'check-circled-outline'; + const glyph = 'check-circle-outline'; const glyphs = { 'stream-wals': 'android-sync', diff --git a/ui/app/models/secret.js b/ui/app/models/secret.js index 52010f857fe5..750f0e44f13f 100644 --- a/ui/app/models/secret.js +++ b/ui/app/models/secret.js @@ -13,6 +13,12 @@ export default DS.Model.extend(KeyMixin, { renewable: attr('boolean'), secretData: attr('object'), + secretKeyAndValue: computed('secretData', function() { + const data = this.get('secretData'); + return Object.keys(data).map(key => { + return { key, value: data[key] }; + }); + }), dataAsJSONString: computed('secretData', function() { return JSON.stringify(this.get('secretData'), null, 2); diff --git a/ui/app/styles/components/popup-menu.scss b/ui/app/styles/components/popup-menu.scss index eef0d28372d5..8e1af4f2ae77 100644 --- a/ui/app/styles/components/popup-menu.scss +++ b/ui/app/styles/components/popup-menu.scss @@ -23,10 +23,6 @@ .menu { padding: $size-11 0; - - small code { - margin-left: $spacing-xs; - } } button.link, diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss index 87706441b2f8..bf865e8b0af8 100644 --- a/ui/app/styles/core.scss +++ b/ui/app/styles/core.scss @@ -30,6 +30,7 @@ @import './core/navbar'; @import './core/notification'; @import './core/progress'; +@import './core/select'; @import './core/switch'; @import './core/tables'; @import './core/tags'; diff --git a/ui/app/styles/core/select.scss b/ui/app/styles/core/select.scss new file mode 100644 index 000000000000..449bf9fb6a05 --- /dev/null +++ b/ui/app/styles/core/select.scss @@ -0,0 +1,3 @@ +.select select { + text-rendering: auto !important; +} diff --git a/ui/app/templates/partials/secret-form-show.hbs b/ui/app/templates/partials/secret-form-show.hbs index c342675e1520..a2bf10e0a6c0 100644 --- a/ui/app/templates/partials/secret-form-show.hbs +++ b/ui/app/templates/partials/secret-form-show.hbs @@ -46,10 +46,10 @@ - {{#each-in modelForData.secretData as |key value|}} - {{#info-table-row label=key value=value alwaysRender=true}} - {{masked-input value=value displayOnly=true allowCopy=true}} + {{#each modelForData.secretKeyAndValue as |secret|}} + {{#info-table-row label=secret.key value=secret.value alwaysRender=true}} + {{masked-input value=secret.value displayOnly=true allowCopy=true}} {{/info-table-row}} - {{/each-in}} + {{/each}} {{/if}} {{/if}} diff --git a/ui/app/templates/partials/status/cluster.hbs b/ui/app/templates/partials/status/cluster.hbs index 81710fa756ea..7527c5a6b157 100644 --- a/ui/app/templates/partials/status/cluster.hbs +++ b/ui/app/templates/partials/status/cluster.hbs @@ -2,78 +2,81 @@