diff --git a/CHANGELOG.md b/CHANGELOG.md index 31a97194b1..734c125c46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,15 @@ FEATURES: * Transit Key Trimming: Keys in transit secret engine can now be trimmed to remove older unused key versions [GH-5388] + * Web UI support for KV Version 2. Browse, delete, undelete and destroy + individual secret versions in the UI. [GH-5547], [GH-5563] IMPROVEMENTS: * auth/token: New tokens are salted using SHA2-256 HMAC instead of SHA1 hash * identity: Identity names will now be handled case insensitively by default. This includes names of entities, aliases and groups [GH-5404] + * secret/azure: Credentials can now be generated against an existing service principal. * secret/database: Allow Cassandra user to be non-superuser so long as it has role creation permissions [GH-5402] * secret/radius: Allow setting the NAS Identifier value in the generated diff --git a/builtin/logical/database/dbplugin/client.go b/builtin/logical/database/dbplugin/client.go index 37cb629c4c..82b43551ca 100644 --- a/builtin/logical/database/dbplugin/client.go +++ b/builtin/logical/database/dbplugin/client.go @@ -32,12 +32,21 @@ func (dc *DatabasePluginClient) Close() error { // plugin. The client is wrapped in a DatabasePluginClient object to ensure the // plugin is killed on call of Close(). func newPluginClient(ctx context.Context, sys pluginutil.RunnerUtil, pluginRunner *pluginutil.PluginRunner, logger log.Logger) (Database, error) { - // pluginMap is the map of plugins we can dispense. - var pluginMap = map[string]plugin.Plugin{ - "database": new(DatabasePlugin), + // pluginSets is the map of plugins we can dispense. + pluginSets := map[int]plugin.PluginSet{ + // Version 3 supports both protocols + 3: plugin.PluginSet{ + "database": &DatabasePlugin{ + GRPCDatabasePlugin: new(GRPCDatabasePlugin), + }, + }, + // Version 4 only supports gRPC + 4: plugin.PluginSet{ + "database": new(GRPCDatabasePlugin), + }, } - client, err := pluginRunner.Run(ctx, sys, pluginMap, handshakeConfig, []string{}, logger) + client, err := pluginRunner.Run(ctx, sys, pluginSets, handshakeConfig, []string{}, logger) if err != nil { return nil, err } @@ -61,7 +70,7 @@ func newPluginClient(ctx context.Context, sys pluginutil.RunnerUtil, pluginRunne case *gRPCClient: db = raw.(*gRPCClient) case *databasePluginRPCClient: - logger.Warn("plugin is using deprecated net RPC transport, recompile plugin to upgrade to gRPC", "plugin", pluginRunner.Name) + logger.Warn("plugin is using deprecated netRPC transport, recompile plugin to upgrade to gRPC", "plugin", pluginRunner.Name) db = raw.(*databasePluginRPCClient) default: return nil, errors.New("unsupported client type") diff --git a/builtin/logical/database/dbplugin/plugin.go b/builtin/logical/database/dbplugin/plugin.go index 502f97ebce..1a39e5e03a 100644 --- a/builtin/logical/database/dbplugin/plugin.go +++ b/builtin/logical/database/dbplugin/plugin.go @@ -26,7 +26,7 @@ type Database interface { Init(ctx context.Context, config map[string]interface{}, verifyConnection bool) (saveConfig map[string]interface{}, err error) Close() error - // DEPRECATED, will be removed in 0.12 + // DEPRECATED, will be removed in 0.13 Initialize(ctx context.Context, config map[string]interface{}, verifyConnection bool) (err error) } @@ -104,25 +104,35 @@ func PluginFactory(ctx context.Context, pluginName string, sys pluginutil.LookRu // This prevents users from executing bad plugins or executing a plugin // directory. It is a UX feature, not a security feature. var handshakeConfig = plugin.HandshakeConfig{ - ProtocolVersion: 3, + ProtocolVersion: 4, MagicCookieKey: "VAULT_DATABASE_PLUGIN", MagicCookieValue: "926a0820-aea2-be28-51d6-83cdf00e8edb", } var _ plugin.Plugin = &DatabasePlugin{} var _ plugin.GRPCPlugin = &DatabasePlugin{} +var _ plugin.Plugin = &GRPCDatabasePlugin{} +var _ plugin.GRPCPlugin = &GRPCDatabasePlugin{} // DatabasePlugin implements go-plugin's Plugin interface. It has methods for // retrieving a server and a client instance of the plugin. type DatabasePlugin struct { - impl Database + *GRPCDatabasePlugin +} + +// GRPCDatabasePlugin is the plugin.Plugin implementation that only supports GRPC +// transport +type GRPCDatabasePlugin struct { + Impl Database + + // Embeding this will disable the netRPC protocol + plugin.NetRPCUnsupportedPlugin } func (d DatabasePlugin) Server(*plugin.MuxBroker) (interface{}, error) { impl := &DatabaseErrorSanitizerMiddleware{ - next: d.impl, + next: d.Impl, } - return &databasePluginRPCServer{impl: impl}, nil } @@ -130,16 +140,16 @@ func (DatabasePlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, e return &databasePluginRPCClient{client: c}, nil } -func (d DatabasePlugin) GRPCServer(_ *plugin.GRPCBroker, s *grpc.Server) error { +func (d GRPCDatabasePlugin) GRPCServer(_ *plugin.GRPCBroker, s *grpc.Server) error { impl := &DatabaseErrorSanitizerMiddleware{ - next: d.impl, + next: d.Impl, } RegisterDatabaseServer(s, &gRPCServer{impl: impl}) return nil } -func (DatabasePlugin) GRPCClient(doneCtx context.Context, _ *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { +func (GRPCDatabasePlugin) GRPCClient(doneCtx context.Context, _ *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { return &gRPCClient{ client: NewDatabaseClient(c), clientConn: c, diff --git a/builtin/logical/database/dbplugin/plugin_test.go b/builtin/logical/database/dbplugin/plugin_test.go index ff04169819..baaacb78d3 100644 --- a/builtin/logical/database/dbplugin/plugin_test.go +++ b/builtin/logical/database/dbplugin/plugin_test.go @@ -127,6 +127,7 @@ func TestPlugin_NetRPC_Main(t *testing.T) { return } + os.Unsetenv(pluginutil.PluginVaultVersionEnv) p := &mockPlugin{ users: make(map[string][]string), } diff --git a/builtin/logical/database/dbplugin/server.go b/builtin/logical/database/dbplugin/server.go index 656c44b2d2..9f17049354 100644 --- a/builtin/logical/database/dbplugin/server.go +++ b/builtin/logical/database/dbplugin/server.go @@ -15,24 +15,34 @@ func Serve(db Database, tlsProvider func() (*tls.Config, error)) { } func ServeConfig(db Database, tlsProvider func() (*tls.Config, error)) *plugin.ServeConfig { - dbPlugin := &DatabasePlugin{ - impl: db, - } - - // pluginMap is the map of plugins we can dispense. - var pluginMap = map[string]plugin.Plugin{ - "database": dbPlugin, + // pluginSets is the map of plugins we can dispense. + pluginSets := map[int]plugin.PluginSet{ + 3: plugin.PluginSet{ + "database": &DatabasePlugin{ + GRPCDatabasePlugin: &GRPCDatabasePlugin{ + Impl: db, + }, + }, + }, + 4: plugin.PluginSet{ + "database": &GRPCDatabasePlugin{ + Impl: db, + }, + }, } conf := &plugin.ServeConfig{ - HandshakeConfig: handshakeConfig, - Plugins: pluginMap, - TLSProvider: tlsProvider, - GRPCServer: plugin.DefaultGRPCServer, + HandshakeConfig: handshakeConfig, + VersionedPlugins: pluginSets, + TLSProvider: tlsProvider, + GRPCServer: plugin.DefaultGRPCServer, } + // If we do not have gRPC support fallback to version 3 + // Remove this block in 0.13 if !pluginutil.GRPCSupport() { conf.GRPCServer = nil + delete(conf.VersionedPlugins, 4) } return conf diff --git a/helper/pluginutil/env.go b/helper/pluginutil/env.go index 337c7b736e..ed40c7fbf8 100644 --- a/helper/pluginutil/env.go +++ b/helper/pluginutil/env.go @@ -35,32 +35,27 @@ func OptionallyEnableMlock() error { // it fails to meet the version constraint. func GRPCSupport() bool { verString := os.Getenv(PluginVaultVersionEnv) - // If the env var is empty, we fall back to netrpc for backward compatibility. if verString == "" { return false } - if verString != "unknown" { ver, err := version.NewVersion(verString) if err != nil { return true } - // Due to some regressions on 0.9.2 & 0.9.3 we now require version 0.9.4 // to allow the plugin framework to default to gRPC. constraint, err := version.NewConstraint(">= 0.9.4") if err != nil { return true } - return constraint.Check(ver) } - return true } -// Returns true if the plugin calling this function is running in metadata mode. +// InMetadataMode returns true if the plugin calling this function is running in metadata mode. func InMetadataMode() bool { return os.Getenv(PluginMetadataModeEnv) == "true" } diff --git a/helper/pluginutil/runner.go b/helper/pluginutil/runner.go index 41b32d9463..74fe95cc24 100644 --- a/helper/pluginutil/runner.go +++ b/helper/pluginutil/runner.go @@ -22,7 +22,7 @@ type Looker interface { LookupPlugin(context.Context, string) (*PluginRunner, error) } -// Wrapper interface defines the functions needed by the runner to wrap the +// RunnerUtil interface defines the functions needed by the runner to wrap the // metadata needed to run a plugin process. This includes looking up Mlock // configuration and wrapping data in a response wrapped token. // logical.SystemView implementations satisfy this interface. @@ -31,7 +31,7 @@ type RunnerUtil interface { MlockEnabled() bool } -// LookWrapper defines the functions for both Looker and Wrapper +// LookRunnerUtil defines the functions for both Looker and Wrapper type LookRunnerUtil interface { Looker RunnerUtil @@ -52,19 +52,19 @@ type PluginRunner struct { // Run takes a wrapper RunnerUtil instance along with the go-plugin parameters and // returns a configured plugin.Client with TLS Configured and a wrapping token set // on PluginUnwrapTokenEnv for plugin process consumption. -func (r *PluginRunner) Run(ctx context.Context, wrapper RunnerUtil, pluginMap map[string]plugin.Plugin, hs plugin.HandshakeConfig, env []string, logger log.Logger) (*plugin.Client, error) { - return r.runCommon(ctx, wrapper, pluginMap, hs, env, logger, false) +func (r *PluginRunner) Run(ctx context.Context, wrapper RunnerUtil, pluginSets map[int]plugin.PluginSet, hs plugin.HandshakeConfig, env []string, logger log.Logger) (*plugin.Client, error) { + return r.runCommon(ctx, wrapper, pluginSets, hs, env, logger, false) } // RunMetadataMode returns a configured plugin.Client that will dispense a plugin // in metadata mode. The PluginMetadataModeEnv is passed in as part of the Cmd to // plugin.Client, and consumed by the plugin process on pluginutil.VaultPluginTLSProvider. -func (r *PluginRunner) RunMetadataMode(ctx context.Context, wrapper RunnerUtil, pluginMap map[string]plugin.Plugin, hs plugin.HandshakeConfig, env []string, logger log.Logger) (*plugin.Client, error) { - return r.runCommon(ctx, wrapper, pluginMap, hs, env, logger, true) +func (r *PluginRunner) RunMetadataMode(ctx context.Context, wrapper RunnerUtil, pluginSets map[int]plugin.PluginSet, hs plugin.HandshakeConfig, env []string, logger log.Logger) (*plugin.Client, error) { + return r.runCommon(ctx, wrapper, pluginSets, hs, env, logger, true) } -func (r *PluginRunner) runCommon(ctx context.Context, wrapper RunnerUtil, pluginMap map[string]plugin.Plugin, hs plugin.HandshakeConfig, env []string, logger log.Logger, isMetadataMode bool) (*plugin.Client, error) { +func (r *PluginRunner) runCommon(ctx context.Context, wrapper RunnerUtil, pluginSets map[int]plugin.PluginSet, hs plugin.HandshakeConfig, env []string, logger log.Logger, isMetadataMode bool) (*plugin.Client, error) { cmd := exec.Command(r.Command, r.Args...) // `env` should always go last to avoid overwriting internal values that might @@ -115,12 +115,12 @@ func (r *PluginRunner) runCommon(ctx context.Context, wrapper RunnerUtil, plugin } clientConfig := &plugin.ClientConfig{ - HandshakeConfig: hs, - Plugins: pluginMap, - Cmd: cmd, - SecureConfig: secureConfig, - TLSConfig: clientTLSConfig, - Logger: logger, + HandshakeConfig: hs, + VersionedPlugins: pluginSets, + Cmd: cmd, + SecureConfig: secureConfig, + TLSConfig: clientTLSConfig, + Logger: logger, AllowedProtocols: []plugin.Protocol{ plugin.ProtocolNetRPC, plugin.ProtocolGRPC, @@ -132,6 +132,8 @@ func (r *PluginRunner) runCommon(ctx context.Context, wrapper RunnerUtil, plugin return client, nil } +// APIClientMeta is a helper that plugins can use to configure TLS connections +// back to Vault. type APIClientMeta struct { // These are set by the command line flags. flagCACert string @@ -141,6 +143,7 @@ type APIClientMeta struct { flagInsecure bool } +// FlagSet returns the flag set for configuring the TLS connection func (f *APIClientMeta) FlagSet() *flag.FlagSet { fs := flag.NewFlagSet("vault plugin settings", flag.ContinueOnError) @@ -153,6 +156,7 @@ func (f *APIClientMeta) FlagSet() *flag.FlagSet { return fs } +// GetTLSConfig will return a TLSConfig based off the values from the flags func (f *APIClientMeta) GetTLSConfig() *api.TLSConfig { // If we need custom TLS configuration, then set it if f.flagCACert != "" || f.flagCAPath != "" || f.flagClientCert != "" || f.flagClientKey != "" || f.flagInsecure { @@ -171,7 +175,7 @@ func (f *APIClientMeta) GetTLSConfig() *api.TLSConfig { return nil } -// CancelIfCanceled takes a context cancel func and a context. If the context is +// CtxCancelIfCanceled takes a context cancel func and a context. If the context is // shutdown the cancelfunc is called. This is useful for merging two cancel // functions. func CtxCancelIfCanceled(f context.CancelFunc, ctxCanceler context.Context) chan struct{} { diff --git a/logical/plugin/backend.go b/logical/plugin/backend.go index b55a0aaefd..ac367c165e 100644 --- a/logical/plugin/backend.go +++ b/logical/plugin/backend.go @@ -13,11 +13,25 @@ import ( "github.com/hashicorp/vault/logical/plugin/pb" ) +var _ plugin.Plugin = (*BackendPlugin)(nil) +var _ plugin.GRPCPlugin = (*BackendPlugin)(nil) +var _ plugin.Plugin = (*GRPCBackendPlugin)(nil) +var _ plugin.GRPCPlugin = (*GRPCBackendPlugin)(nil) + // BackendPlugin is the plugin.Plugin implementation type BackendPlugin struct { + *GRPCBackendPlugin +} + +// GRPCBackendPlugin is the plugin.Plugin implementation that only supports GRPC +// transport +type GRPCBackendPlugin struct { Factory logical.Factory - metadataMode bool + MetadataMode bool Logger log.Logger + + // Embeding this will disable the netRPC protocol + plugin.NetRPCUnsupportedPlugin } // Server gets called when on plugin.Serve() @@ -33,10 +47,14 @@ func (b *BackendPlugin) Server(broker *plugin.MuxBroker) (interface{}, error) { // Client gets called on plugin.NewClient() func (b BackendPlugin) Client(broker *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { - return &backendPluginClient{client: c, broker: broker, metadataMode: b.metadataMode}, nil + return &backendPluginClient{ + client: c, + broker: broker, + metadataMode: b.MetadataMode, + }, nil } -func (b BackendPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { +func (b GRPCBackendPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { pb.RegisterBackendServer(s, &backendGRPCPluginServer{ broker: broker, factory: b.Factory, @@ -47,13 +65,14 @@ func (b BackendPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) err return nil } -func (p *BackendPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { +func (b *GRPCBackendPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { ret := &backendGRPCPluginClient{ - client: pb.NewBackendClient(c), - clientConn: c, - broker: broker, - cleanupCh: make(chan struct{}), - doneCtx: ctx, + client: pb.NewBackendClient(c), + clientConn: c, + broker: broker, + cleanupCh: make(chan struct{}), + doneCtx: ctx, + metadataMode: b.MetadataMode, } // Create the value and set the type diff --git a/logical/plugin/backend_test.go b/logical/plugin/backend_test.go index 3294506493..d36d7639f2 100644 --- a/logical/plugin/backend_test.go +++ b/logical/plugin/backend_test.go @@ -140,7 +140,9 @@ func testBackend(t *testing.T) (logical.Backend, func()) { // Create a mock provider pluginMap := map[string]gplugin.Plugin{ "backend": &BackendPlugin{ - Factory: mock.Factory, + GRPCBackendPlugin: &GRPCBackendPlugin{ + Factory: mock.Factory, + }, }, } client, _ := gplugin.TestPluginRPCConn(t, pluginMap, nil) diff --git a/logical/plugin/grpc_backend_test.go b/logical/plugin/grpc_backend_test.go index 3448152944..63d1251393 100644 --- a/logical/plugin/grpc_backend_test.go +++ b/logical/plugin/grpc_backend_test.go @@ -141,12 +141,14 @@ func testGRPCBackend(t *testing.T) (logical.Backend, func()) { // Create a mock provider pluginMap := map[string]gplugin.Plugin{ "backend": &BackendPlugin{ - Factory: mock.Factory, - Logger: log.New(&log.LoggerOptions{ - Level: log.Debug, - Output: os.Stderr, - JSONFormat: true, - }), + GRPCBackendPlugin: &GRPCBackendPlugin{ + Factory: mock.Factory, + Logger: log.New(&log.LoggerOptions{ + Level: log.Debug, + Output: os.Stderr, + JSONFormat: true, + }), + }, }, } client, _ := gplugin.TestPluginGRPCConn(t, pluginMap) diff --git a/logical/plugin/plugin.go b/logical/plugin/plugin.go index 754494b8f8..7ef3a55770 100644 --- a/logical/plugin/plugin.go +++ b/logical/plugin/plugin.go @@ -96,9 +96,18 @@ func NewBackend(ctx context.Context, pluginName string, sys pluginutil.LookRunne func newPluginClient(ctx context.Context, sys pluginutil.RunnerUtil, pluginRunner *pluginutil.PluginRunner, logger log.Logger, isMetadataMode bool) (logical.Backend, error) { // pluginMap is the map of plugins we can dispense. - pluginMap := map[string]plugin.Plugin{ - "backend": &BackendPlugin{ - metadataMode: isMetadataMode, + pluginSet := map[int]plugin.PluginSet{ + 3: plugin.PluginSet{ + "backend": &BackendPlugin{ + GRPCBackendPlugin: &GRPCBackendPlugin{ + MetadataMode: isMetadataMode, + }, + }, + }, + 4: plugin.PluginSet{ + "backend": &GRPCBackendPlugin{ + MetadataMode: isMetadataMode, + }, }, } @@ -107,9 +116,9 @@ func newPluginClient(ctx context.Context, sys pluginutil.RunnerUtil, pluginRunne var client *plugin.Client var err error if isMetadataMode { - client, err = pluginRunner.RunMetadataMode(ctx, sys, pluginMap, handshakeConfig, []string{}, namedLogger) + client, err = pluginRunner.RunMetadataMode(ctx, sys, pluginSet, handshakeConfig, []string{}, namedLogger) } else { - client, err = pluginRunner.Run(ctx, sys, pluginMap, handshakeConfig, []string{}, namedLogger) + client, err = pluginRunner.Run(ctx, sys, pluginSet, handshakeConfig, []string{}, namedLogger) } if err != nil { return nil, err @@ -133,6 +142,7 @@ func newPluginClient(ctx context.Context, sys pluginutil.RunnerUtil, pluginRunne // implementation but is in fact over an RPC connection. switch raw.(type) { case *backendPluginClient: + logger.Warn("plugin is using deprecated netRPC transport, recompile plugin to upgrade to gRPC", "plugin", pluginRunner.Name) backend = raw.(*backendPluginClient) transport = "netRPC" case *backendGRPCPluginClient: diff --git a/logical/plugin/serve.go b/logical/plugin/serve.go index 9dd57f8d7a..97b9f28bd6 100644 --- a/logical/plugin/serve.go +++ b/logical/plugin/serve.go @@ -14,7 +14,7 @@ import ( ) // BackendPluginName is the name of the plugin that can be -// dispensed rom the plugin server. +// dispensed from the plugin server. const BackendPluginName = "backend" type TLSProviderFunc func() (*tls.Config, error) @@ -38,10 +38,20 @@ func Serve(opts *ServeOpts) error { } // pluginMap is the map of plugins we can dispense. - var pluginMap = map[string]plugin.Plugin{ - "backend": &BackendPlugin{ - Factory: opts.BackendFactoryFunc, - Logger: logger, + pluginSets := map[int]plugin.PluginSet{ + 3: plugin.PluginSet{ + "backend": &BackendPlugin{ + GRPCBackendPlugin: &GRPCBackendPlugin{ + Factory: opts.BackendFactoryFunc, + Logger: logger, + }, + }, + }, + 4: plugin.PluginSet{ + "backend": &GRPCBackendPlugin{ + Factory: opts.BackendFactoryFunc, + Logger: logger, + }, }, } @@ -51,10 +61,10 @@ func Serve(opts *ServeOpts) error { } serveOpts := &plugin.ServeConfig{ - HandshakeConfig: handshakeConfig, - Plugins: pluginMap, - TLSProvider: opts.TLSProviderFunc, - Logger: logger, + HandshakeConfig: handshakeConfig, + VersionedPlugins: pluginSets, + TLSProvider: opts.TLSProviderFunc, + Logger: logger, // A non-nil value here enables gRPC serving for this plugin... GRPCServer: func(opts []grpc.ServerOption) *grpc.Server { @@ -64,11 +74,13 @@ func Serve(opts *ServeOpts) error { }, } + // If we do not have gRPC support fallback to version 3 + // Remove this block in 0.13 if !pluginutil.GRPCSupport() { serveOpts.GRPCServer = nil + delete(pluginSets, 4) } - // If FetchMetadata is true, run without TLSProvider plugin.Serve(serveOpts) return nil @@ -79,7 +91,7 @@ func Serve(opts *ServeOpts) error { // This prevents users from executing bad plugins or executing a plugin // directory. It is a UX feature, not a security feature. var handshakeConfig = plugin.HandshakeConfig{ - ProtocolVersion: 3, + ProtocolVersion: 4, MagicCookieKey: "VAULT_BACKEND_PLUGIN", MagicCookieValue: "6669da05-b1c8-4f49-97d9-c8e5bed98e20", } diff --git a/ui/app/components/list-item.js b/ui/app/components/list-item.js index e4f1f0c0fa..c0a4396d11 100644 --- a/ui/app/components/list-item.js +++ b/ui/app/components/list-item.js @@ -7,7 +7,7 @@ export default Component.extend({ tagName: '', linkParams: null, componentName: null, - hasMenu: false, + hasMenu: true, callMethod: task(function*(method, model, successMessage, failureMessage, successCallback = () => {}) { let flash = this.get('flashMessages'); diff --git a/ui/app/components/list-item/popup-menu.js b/ui/app/components/list-item/popup-menu.js index 4798652642..b1f4a59f1d 100644 --- a/ui/app/components/list-item/popup-menu.js +++ b/ui/app/components/list-item/popup-menu.js @@ -2,4 +2,6 @@ import Component from '@ember/component'; export default Component.extend({ tagName: '', + item: null, + hasMenu: null, }); diff --git a/ui/app/components/secret-edit.js b/ui/app/components/secret-edit.js index b83d451ee6..4b0e684778 100644 --- a/ui/app/components/secret-edit.js +++ b/ui/app/components/secret-edit.js @@ -1,9 +1,8 @@ -import { or } from '@ember/object/computed'; import { isBlank, isNone } from '@ember/utils'; import { inject as service } from '@ember/service'; import Component from '@ember/component'; import { computed, set } from '@ember/object'; -import { alias } from '@ember/object/computed'; +import { alias, or } from '@ember/object/computed'; import { task, waitForEvent } from 'ember-concurrency'; import FocusOnInsertMixin from 'vault/mixins/focus-on-insert'; import keys from 'vault/lib/keycodes'; @@ -127,49 +126,6 @@ export default Component.extend(FocusOnInsertMixin, { ), canEditV2Secret: alias('v2UpdatePath.canUpdate'), - deleteVersionPath: maybeQueryRecord( - 'capabilities', - context => { - let backend = context.get('model.engine.id'); - let id = context.model.id; - return { - id: `${backend}/delete/${id}`, - }; - }, - 'model.id' - ), - canDeleteVersion: alias('deleteVersionPath.canUpdate'), - destroyVersionPath: maybeQueryRecord( - 'capabilities', - context => { - let backend = context.get('model.engine.id'); - let id = context.model.id; - return { - id: `${backend}/destroy/${id}`, - }; - }, - 'model.id' - ), - canDestroyVersion: alias('destroyVersionPath.canUpdate'), - undeleteVersionPath: maybeQueryRecord( - 'capabilities', - context => { - let backend = context.get('model.engine.id'); - let id = context.model.id; - return { - id: `${backend}/undelete/${id}`, - }; - }, - 'model.id' - ), - canUndeleteVersion: alias('undeleteVersionPath.canUpdate'), - - isFetchingVersionCapabilities: or( - 'deleteVersionPath.isPending', - 'destroyVersionPath.isPending', - 'undeleteVersionPath.isPending' - ), - requestInFlight: or('model.isLoading', 'model.isReloading', 'model.isSaving'), buttonDisabled: or( @@ -299,11 +255,6 @@ export default Component.extend(FocusOnInsertMixin, { }); }, - deleteVersion(deleteType = 'destroy') { - let id = this.modelForData.id; - return this.store.adapterFor('secret-v2-version').v2DeleteOperation(this.store, id, deleteType); - }, - refresh() { this.onRefresh(); }, diff --git a/ui/app/components/secret-version-menu.js b/ui/app/components/secret-version-menu.js new file mode 100644 index 0000000000..5b13c6cd21 --- /dev/null +++ b/ui/app/components/secret-version-menu.js @@ -0,0 +1,58 @@ +import { maybeQueryRecord } from 'vault/macros/maybe-query-record'; +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; +import { alias, or } from '@ember/object/computed'; + +export default Component.extend({ + tagName: '', + store: service(), + version: null, + useDefaultTrigger: false, + + deleteVersionPath: maybeQueryRecord( + 'capabilities', + context => { + let [backend, id] = JSON.parse(context.version.id); + return { + id: `${backend}/delete/${id}`, + }; + }, + 'version.id' + ), + canDeleteVersion: alias('deleteVersionPath.canUpdate'), + destroyVersionPath: maybeQueryRecord( + 'capabilities', + context => { + let [backend, id] = JSON.parse(context.version.id); + return { + id: `${backend}/destroy/${id}`, + }; + }, + 'version.id' + ), + canDestroyVersion: alias('destroyVersionPath.canUpdate'), + undeleteVersionPath: maybeQueryRecord( + 'capabilities', + context => { + let [backend, id] = JSON.parse(context.version.id); + return { + id: `${backend}/undelete/${id}`, + }; + }, + 'version.id' + ), + canUndeleteVersion: alias('undeleteVersionPath.canUpdate'), + + isFetchingVersionCapabilities: or( + 'deleteVersionPath.isPending', + 'destroyVersionPath.isPending', + 'undeleteVersionPath.isPending' + ), + actions: { + deleteVersion(deleteType = 'destroy') { + return this.store + .adapterFor('secret-v2-version') + .v2DeleteOperation(this.store, this.version.id, deleteType); + }, + }, +}); diff --git a/ui/app/router.js b/ui/app/router.js index 5660a3e2c2..1301330180 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -92,6 +92,10 @@ Router.map(function() { this.route('credentials-root', { path: '/credentials/' }); this.route('credentials', { path: '/credentials/*secret' }); + // kv v2 versions + this.route('versions-root', { path: '/versions/' }); + this.route('versions', { path: '/versions/*secret' }); + // ssh sign this.route('sign-root', { path: '/sign/' }); this.route('sign', { path: '/sign/*secret' }); diff --git a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js index 42da97bf16..873fc305f6 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js +++ b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js @@ -140,7 +140,7 @@ export default Route.extend(UnloadModelRoute, { }, willTransition(transition) { - let model = this.controller.model; + let { mode, model } = this.controller; let version = model.get('selectedVersion'); let changed = model.changedAttributes(); let changedKeys = Object.keys(changed); @@ -148,8 +148,8 @@ export default Route.extend(UnloadModelRoute, { // it's going to dirty the model state, so we need to look for it // and explicity ignore it here if ( - (changedKeys.length && changedKeys[0] !== 'backend') || - (version && Object.keys(version.changedAttributes()).length) + (mode !== 'show' && (changedKeys.length && changedKeys[0] !== 'backend')) || + (mode !== 'show' && version && Object.keys(version.changedAttributes()).length) ) { if ( window.confirm( diff --git a/ui/app/routes/vault/cluster/secrets/backend/versions-root.js b/ui/app/routes/vault/cluster/secrets/backend/versions-root.js new file mode 100644 index 0000000000..69de93ac8a --- /dev/null +++ b/ui/app/routes/vault/cluster/secrets/backend/versions-root.js @@ -0,0 +1 @@ +export { default } from './version'; diff --git a/ui/app/routes/vault/cluster/secrets/backend/versions.js b/ui/app/routes/vault/cluster/secrets/backend/versions.js new file mode 100644 index 0000000000..da1d44097e --- /dev/null +++ b/ui/app/routes/vault/cluster/secrets/backend/versions.js @@ -0,0 +1,27 @@ +import Route from '@ember/routing/route'; +import utils from 'vault/lib/key-utils'; +import UnloadModelRoute from 'vault/mixins/unload-model-route'; + +export default Route.extend(UnloadModelRoute, { + templateName: 'vault/cluster/secrets/backend/versions', + + beforeModel() { + let backendModel = this.modelFor('vault.cluster.secrets.backend'); + const { secret } = this.paramsFor(this.routeName); + const parentKey = utils.parentKeyForKey(secret); + if (backendModel.get('isV2KV')) { + return; + } + if (parentKey) { + return this.transitionTo('vault.cluster.secrets.backend.list', parentKey); + } else { + return this.transitionTo('vault.cluster.secrets.backend.list-root'); + } + }, + + model(params) { + let { secret } = params; + const { backend } = this.paramsFor('vault.cluster.secrets.backend'); + return this.store.queryRecord('secret-v2', { id: secret, backend }); + }, +}); diff --git a/ui/app/serializers/secret-v2.js b/ui/app/serializers/secret-v2.js index e1d7fc7f6c..0be535c778 100644 --- a/ui/app/serializers/secret-v2.js +++ b/ui/app/serializers/secret-v2.js @@ -38,6 +38,7 @@ export default ApplicationSerializer.extend(DS.EmbeddedRecordsMixin, { return body; }); } + payload.data.engine_id = payload.backend; payload.data.id = payload.id; return requestType === 'queryRecord' ? payload.data : [payload.data]; }, diff --git a/ui/app/styles/core/helpers.scss b/ui/app/styles/core/helpers.scss index b45b46732d..3f94d9115a 100644 --- a/ui/app/styles/core/helpers.scss +++ b/ui/app/styles/core/helpers.scss @@ -4,12 +4,18 @@ .is-underline { text-decoration: underline; } +.is-no-underline { + text-decoration: none; +} .is-sideless { box-shadow: 0 2px 0 -1px $grey-light, 0 -2px 0 -1px $grey-light; } .is-bottomless { box-shadow: 0 -1px 0 0 $grey-light; } +.has-bottom-shadow { + box-shadow: $box-shadow !important; +} .is-borderless { border: none !important; } diff --git a/ui/app/templates/components/list-item.hbs b/ui/app/templates/components/list-item.hbs index dbb4e13072..fa760d3c56 100644 --- a/ui/app/templates/components/list-item.hbs +++ b/ui/app/templates/components/list-item.hbs @@ -3,18 +3,14 @@ {{else if linkParams}}
-
-
- {{#link-to params=linkParams class="has-text-weight-semibold"}} - {{yield (hash content=(component "list-item/content"))}} - {{/link-to}} -
-
+
+ {{#link-to params=linkParams class="has-text-weight-semibold has-text-black is-display-flex is-flex-1 is-no-underline"}} + {{yield (hash content=(component "list-item/content"))}} + {{/link-to}} +
- {{#if hasBlock}} - {{yield (hash callMethod=callMethod menu=(component "list-item/popup-menu"))}} - {{/if}} + {{yield (hash callMethod=callMethod menu=(component "list-item/popup-menu" item=item hasMenu=hasMenu))}}
@@ -22,14 +18,12 @@ {{else}}
-
-
- {{yield (hash content=(component "list-item/content"))}} -
+
+ {{yield (hash content=(component "list-item/content"))}}
- {{yield (hash callMethod=callMethod menu=(component "list-item/popup-menu"))}} + {{yield (hash callMethod=callMethod menu=(component "list-item/popup-menu" item=item hasMenu=hasMenu))}}
diff --git a/ui/app/templates/components/list-item/popup-menu.hbs b/ui/app/templates/components/list-item/popup-menu.hbs index c45c50ca96..9823c5e91c 100644 --- a/ui/app/templates/components/list-item/popup-menu.hbs +++ b/ui/app/templates/components/list-item/popup-menu.hbs @@ -1,7 +1,11 @@ - - - +{{#if hasMenu}} + + + +{{else}} + {{yield item}} +{{/if}} diff --git a/ui/app/templates/components/secret-edit.hbs b/ui/app/templates/components/secret-edit.hbs index 582a6fc9fc..d814e73c22 100644 --- a/ui/app/templates/components/secret-edit.hbs +++ b/ui/app/templates/components/secret-edit.hbs @@ -77,130 +77,53 @@ {{/if}} {{#if (and (eq @mode "show") this.isV2)}}
- - - Version {{this.modelForData.version}} - - - - - - +
- - - History - - - - - + + History + + + + +
{{/if}}
diff --git a/ui/app/templates/components/secret-version-menu.hbs b/ui/app/templates/components/secret-version-menu.hbs new file mode 100644 index 0000000000..60e94794bf --- /dev/null +++ b/ui/app/templates/components/secret-version-menu.hbs @@ -0,0 +1,96 @@ + + + {{#if useDefaultTrigger}} + + {{else}} + Version {{this.version.version}} + + {{/if}} + + + + + \ No newline at end of file diff --git a/ui/app/templates/partials/secret-list/item.hbs b/ui/app/templates/partials/secret-list/item.hbs index 8c60ef3844..0363fe2b52 100644 --- a/ui/app/templates/partials/secret-list/item.hbs +++ b/ui/app/templates/partials/secret-list/item.hbs @@ -23,7 +23,7 @@
- +