mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 03:27:54 +00:00
Merge branch 'oss-master' into 1.0-beta-oss
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -127,6 +127,7 @@ func TestPlugin_NetRPC_Main(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
os.Unsetenv(pluginutil.PluginVaultVersionEnv)
|
||||
p := &mockPlugin{
|
||||
users: make(map[string][]string),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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{} {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -2,4 +2,6 @@ import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
item: null,
|
||||
hasMenu: null,
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
58
ui/app/components/secret-version-menu.js
Normal file
58
ui/app/components/secret-version-menu.js
Normal file
@@ -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);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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' });
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './version';
|
||||
27
ui/app/routes/vault/cluster/secrets/backend/versions.js
Normal file
27
ui/app/routes/vault/cluster/secrets/backend/versions.js
Normal file
@@ -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 });
|
||||
},
|
||||
});
|
||||
@@ -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];
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -3,18 +3,14 @@
|
||||
{{else if linkParams}}
|
||||
<LinkedBlock @params={{linkParams}} @class="list-item-row">
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left">
|
||||
<div>
|
||||
{{#link-to params=linkParams class="has-text-weight-semibold"}}
|
||||
{{yield (hash content=(component "list-item/content"))}}
|
||||
{{/link-to}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-left is-flex-1">
|
||||
{{#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}}
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
{{#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))}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -22,14 +18,12 @@
|
||||
{{else}}
|
||||
<div class="list-item-row">
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left">
|
||||
<div class="has-text-grey has-text-weight-semibold">
|
||||
{{yield (hash content=(component "list-item/content"))}}
|
||||
</div>
|
||||
<div class="level-left is-flex-1 has-text-weight-semibold">
|
||||
{{yield (hash content=(component "list-item/content"))}}
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
{{yield (hash callMethod=callMethod menu=(component "list-item/popup-menu"))}}
|
||||
{{yield (hash callMethod=callMethod menu=(component "list-item/popup-menu" item=item hasMenu=hasMenu))}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<PopupMenu>
|
||||
<nav class="menu">
|
||||
<ul class="menu-list">
|
||||
{{yield item}}
|
||||
</ul>
|
||||
</nav>
|
||||
</PopupMenu>
|
||||
{{#if hasMenu}}
|
||||
<PopupMenu>
|
||||
<nav class="menu">
|
||||
<ul class="menu-list">
|
||||
{{yield item}}
|
||||
</ul>
|
||||
</nav>
|
||||
</PopupMenu>
|
||||
{{else}}
|
||||
{{yield item}}
|
||||
{{/if}}
|
||||
|
||||
@@ -77,130 +77,53 @@
|
||||
{{/if}}
|
||||
{{#if (and (eq @mode "show") this.isV2)}}
|
||||
<div class="control">
|
||||
<BasicDropdown
|
||||
@class="popup-menu"
|
||||
@horizontalPosition="auto-right"
|
||||
@verticalPosition="below"
|
||||
as |D|
|
||||
>
|
||||
<D.trigger
|
||||
data-test-popup-menu-trigger="true"
|
||||
@class={{concat "popup-menu-trigger button is-ghost has-text-grey" (if D.isOpen " is-active")}}
|
||||
@tagName="button"
|
||||
>
|
||||
Version {{this.modelForData.version}}
|
||||
<ICon @glyph="chevron-right" @size="11" />
|
||||
</D.trigger>
|
||||
<D.content @class="popup-menu-content ">
|
||||
<nav class="box menu">
|
||||
<ul class="menu-list">
|
||||
{{#if this.modelForData.destroyed}}
|
||||
<li class="action has-text-grey">
|
||||
<button type="button" class="link" disabled >
|
||||
The data for {{this.model.id}} version {{this.modelForData.version}} has been destroyed.
|
||||
</button>
|
||||
</li>
|
||||
{{else}}
|
||||
{{#if isFetchingVersionCapabilities}}
|
||||
<li class="action">
|
||||
<button disabled=true type="button" class="link button is-loading is-transparent">
|
||||
loading
|
||||
</button>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="action">
|
||||
{{#if this.modelForData.deleted}}
|
||||
{{#if canUndeleteVersion}}
|
||||
<button type="button" class="link" {{action "deleteVersion" "undelete"}}>
|
||||
Undelete version
|
||||
</button>
|
||||
{{else}}
|
||||
<button type="button" class="link" disabled >
|
||||
The data for {{this.model.id}} version {{this.modelForData.version}} has been deleted. You do not have the permisssion to undelete it.
|
||||
</button>
|
||||
{{/if}}
|
||||
{{else if canDeleteVersion}}
|
||||
<ConfirmAction
|
||||
@buttonClasses="link has-text-danger"
|
||||
@containerClasses="message-body is-block"
|
||||
@messageClasses="is-block"
|
||||
@onConfirmAction={{action "deleteVersion" "delete"}}
|
||||
@confirmMessage={{
|
||||
concat "Are you sure you want to delete " model.id " version " this.modelForData.version "?"
|
||||
}}
|
||||
@cancelButtonText="Cancel"
|
||||
data-test-secret-v2-delete="true"
|
||||
>
|
||||
Delete version
|
||||
</ConfirmAction>
|
||||
{{else}}
|
||||
<button type="button" class="link" disabled >
|
||||
You do not have the permissions to delete the data for this secret.
|
||||
</button>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{#if canDestroyVersion}}
|
||||
<li class="action">
|
||||
<ConfirmAction
|
||||
@buttonClasses="link has-text-danger"
|
||||
@containerClasses="message-body is-block"
|
||||
@messageClasses="is-block"
|
||||
@onConfirmAction={{action "deleteVersion" "destroy"}}
|
||||
@confirmMessage={{
|
||||
concat "This will permanently destroy " model.id " version " this.modelForData.version ". Are you sure you want to do this?"
|
||||
}}
|
||||
@cancelButtonText="Cancel"
|
||||
data-test-secret-v2-destroy="true"
|
||||
>
|
||||
Permanently destroy version
|
||||
</ConfirmAction>
|
||||
</li>
|
||||
{{else}}
|
||||
<button type="button" class="link" disabled >
|
||||
You do not have the permissions to destroy the data for this secret.
|
||||
</button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</ul>
|
||||
</nav>
|
||||
</D.content>
|
||||
</BasicDropdown>
|
||||
<SecretVersionMenu @version={{this.modelForData}} />
|
||||
</div>
|
||||
<div class="control">
|
||||
<BasicDropdown
|
||||
@class="popup-menu"
|
||||
@horizontalPosition="auto-right"
|
||||
@verticalPosition="below"
|
||||
as |D|
|
||||
>
|
||||
<D.trigger
|
||||
data-test-popup-menu-trigger="true"
|
||||
@class={{concat "popup-menu-trigger button is-ghost has-text-grey" (if D.isOpen " is-active")}}
|
||||
@tagName="button"
|
||||
<BasicDropdown
|
||||
@class="popup-menu"
|
||||
@horizontalPosition="auto-right"
|
||||
@verticalPosition="below"
|
||||
as |D|
|
||||
>
|
||||
History <ICon @glyph="chevron-right" @size="11" />
|
||||
</D.trigger>
|
||||
<D.content @class="popup-menu-content ">
|
||||
<nav class="box menu">
|
||||
<h5 class="list-header">Versions</h5>
|
||||
<ul class="menu-list">
|
||||
{{#each (reverse this.model.versions) as |secretVersion|}}
|
||||
<li class="action">
|
||||
<LinkTo class="link" @params={{array (query-params version=secretVersion.version)}}>
|
||||
Version {{secretVersion.version}}
|
||||
{{#if (eq secretVersion.version this.model.currentVersion)}}
|
||||
<ICon @glyph="checkmark-circled-outline" @excludeIconClass={{true}} @size="13" @class="has-text-success is-pulled-right" />
|
||||
{{else if secretVersion.deleted}}
|
||||
<ICon @glyph="cancel-square-outline" @size="13" @excludeIconClass={{true}} @class="has-text-grey is-pulled-right" />
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</nav>
|
||||
</D.content>
|
||||
</BasicDropdown>
|
||||
<D.trigger
|
||||
data-test-popup-menu-trigger="true"
|
||||
@class={{concat "popup-menu-trigger button is-ghost has-text-grey" (if D.isOpen " is-active")}}
|
||||
@tagName="button"
|
||||
>
|
||||
History <ICon @glyph="chevron-right" @size="11" />
|
||||
</D.trigger>
|
||||
<D.content @class="popup-menu-content ">
|
||||
<nav class="box menu">
|
||||
<ul class="menu-list">
|
||||
<li class="action">
|
||||
<SecretLink
|
||||
@mode="versions"
|
||||
@secret={{this.model.id}}
|
||||
@class="has-text-black has-text-weight-semibold has-bottom-shadow"
|
||||
>
|
||||
View version history
|
||||
</SecretLink>
|
||||
</li>
|
||||
</ul>
|
||||
<h5 class="list-header">Versions</h5>
|
||||
<ul class="menu-list">
|
||||
{{#each (reverse this.model.versions) as |secretVersion|}}
|
||||
<li class="action">
|
||||
<LinkTo class="link" @params={{array (query-params version=secretVersion.version)}}>
|
||||
Version {{secretVersion.version}}
|
||||
{{#if (eq secretVersion.version this.model.currentVersion)}}
|
||||
<ICon @glyph="checkmark-circled-outline" @excludeIconClass={{true}} @size="13" @class="has-text-success is-pulled-right" />
|
||||
{{else if secretVersion.deleted}}
|
||||
<ICon @glyph="cancel-square-outline" @size="13" @excludeIconClass={{true}} @class="has-text-grey is-pulled-right" />
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</nav>
|
||||
</D.content>
|
||||
</BasicDropdown>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
96
ui/app/templates/components/secret-version-menu.hbs
Normal file
96
ui/app/templates/components/secret-version-menu.hbs
Normal file
@@ -0,0 +1,96 @@
|
||||
<BasicDropdown
|
||||
@class="popup-menu"
|
||||
@horizontalPosition="auto-right"
|
||||
@verticalPosition="below"
|
||||
as |D|
|
||||
>
|
||||
<D.trigger
|
||||
data-test-popup-menu-trigger="true"
|
||||
@class={{concat "popup-menu-trigger button is-ghost has-text-grey" (if D.isOpen " is-active")}}
|
||||
@tagName="button"
|
||||
>
|
||||
{{#if useDefaultTrigger}}
|
||||
<ICon aria-label="More options" @glyph="more" @size="16" @class="has-text-black auto-width" />
|
||||
{{else}}
|
||||
Version {{this.version.version}}
|
||||
<ICon @glyph="chevron-right" @size="11" />
|
||||
{{/if}}
|
||||
</D.trigger>
|
||||
<D.content @class="popup-menu-content ">
|
||||
<nav class="box menu">
|
||||
<ul class="menu-list">
|
||||
{{#if hasBlock}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
{{#if this.version.destroyed}}
|
||||
<li class="action has-text-grey">
|
||||
<button type="button" class="link" disabled >
|
||||
The data for {{this.version.path}} version {{this.version.version}} has been destroyed.
|
||||
</button>
|
||||
</li>
|
||||
{{else}}
|
||||
{{#if isFetchingVersionCapabilities}}
|
||||
<li class="action">
|
||||
<button disabled=true type="button" class="link button is-loading is-transparent">
|
||||
loading
|
||||
</button>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="action">
|
||||
{{#if this.version.deleted}}
|
||||
{{#if canUndeleteVersion}}
|
||||
<button type="button" class="link" {{action "deleteVersion" "undelete"}}>
|
||||
Undelete version
|
||||
</button>
|
||||
{{else}}
|
||||
<button type="button" class="link" disabled >
|
||||
The data for {{this.version.path}} version {{this.version.version}} has been deleted. You do not have the permisssion to undelete it.
|
||||
</button>
|
||||
{{/if}}
|
||||
{{else if canDeleteVersion}}
|
||||
<ConfirmAction
|
||||
@buttonClasses="link has-text-danger"
|
||||
@containerClasses="message-body is-block"
|
||||
@messageClasses="is-block"
|
||||
@onConfirmAction={{action "deleteVersion" "delete"}}
|
||||
@confirmMessage={{
|
||||
concat "Are you sure you want to delete " this.version.path " version " this.version.version "?"
|
||||
}}
|
||||
@cancelButtonText="Cancel"
|
||||
data-test-secret-v2-delete="true"
|
||||
>
|
||||
Delete version
|
||||
</ConfirmAction>
|
||||
{{else}}
|
||||
<button type="button" class="link" disabled >
|
||||
You do not have the permissions to delete the data for this secret.
|
||||
</button>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{#if canDestroyVersion}}
|
||||
<li class="action">
|
||||
<ConfirmAction
|
||||
@buttonClasses="link has-text-danger"
|
||||
@containerClasses="message-body is-block"
|
||||
@messageClasses="is-block"
|
||||
@onConfirmAction={{action "deleteVersion" "destroy"}}
|
||||
@confirmMessage={{
|
||||
concat "This will permanently destroy " this.version.path " version " this.version.version ". Are you sure you want to do this?"
|
||||
}}
|
||||
@cancelButtonText="Cancel"
|
||||
data-test-secret-v2-destroy="true"
|
||||
>
|
||||
Permanently destroy version
|
||||
</ConfirmAction>
|
||||
</li>
|
||||
{{else}}
|
||||
<button type="button" class="link" disabled >
|
||||
You do not have the permissions to destroy the data for this secret.
|
||||
</button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</ul>
|
||||
</nav>
|
||||
</D.content>
|
||||
</BasicDropdown>
|
||||
@@ -23,7 +23,7 @@
|
||||
</SecretLink>
|
||||
</div>
|
||||
<div class="column has-text-right">
|
||||
<PopupMenu name="secret-menu" @contentClass="is-wide">
|
||||
<PopupMenu name="secret-menu">
|
||||
<nav class="menu">
|
||||
<ul class="menu-list">
|
||||
{{#if item.isFolder}}
|
||||
@@ -52,13 +52,17 @@
|
||||
Details
|
||||
</SecretLink>
|
||||
</li>
|
||||
{{!-- // will add a link to the history view once it exists
|
||||
{{#if backendModel.isV2KV}}
|
||||
<li class="action">
|
||||
Verion History
|
||||
<SecretLink
|
||||
@mode="versions"
|
||||
@secret={{item.id}}
|
||||
@class="has-text-black has-text-weight-semibold"
|
||||
>
|
||||
View version history
|
||||
</SecretLink>
|
||||
</li>
|
||||
{{/if}}
|
||||
--}}
|
||||
{{/if}}
|
||||
{{#if item.canEdit}}
|
||||
<li class="action">
|
||||
|
||||
73
ui/app/templates/vault/cluster/secrets/backend/versions.hbs
Normal file
73
ui/app/templates/vault/cluster/secrets/backend/versions.hbs
Normal file
@@ -0,0 +1,73 @@
|
||||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
{{key-value-header
|
||||
baseKey=(hash id=model.id)
|
||||
path="vault.cluster.secrets.backend.list"
|
||||
mode="show"
|
||||
root=(hash
|
||||
label=model.engineId
|
||||
text=model.engineId
|
||||
path="vault.cluster.secrets.backend.list-root"
|
||||
model=model.engineId
|
||||
)
|
||||
showCurrent=true
|
||||
}}
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
Version History
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
<ListView @items={{reverse model.versions}} @itemNoun="version" as |list|>
|
||||
<ListItem @hasMenu={{false}} @linkParams={{array 'vault.cluster.secrets.backend.show' model.id (query-params version=list.item.version) }} as |Item|>
|
||||
<Item.content>
|
||||
<div class="columns is-flex-1">
|
||||
<div class="column is-one-third">
|
||||
<ICon @glyph="document" @size="18" @class="has-text-grey" />Version {{list.item.version}}
|
||||
{{#if (eq list.item.version model.currentVersion)}}
|
||||
<span class="has-text-success is-size-9">
|
||||
<ICon @glyph="checkmark-circled-outline" @size="13" />Current
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="column">
|
||||
{{#if list.item.deleted}}
|
||||
<span class="has-text-grey is-size-8">
|
||||
<ICon @glyph="false" @size="16" />Deleted
|
||||
</span>
|
||||
{{/if}}
|
||||
{{#if list.item.destroyed}}
|
||||
<span class="has-text-danger is-size-8">
|
||||
<ICon @glyph="cancel-square-outline" @size="16" />Destroyed
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</Item.content>
|
||||
<Item.menu>
|
||||
<SecretVersionMenu @version={{list.item}} @useDefaultTrigger={{true}}>
|
||||
<li class="action">
|
||||
<SecretLink
|
||||
@mode="show"
|
||||
@secret={{model.id}}
|
||||
@class="has-text-black has-text-weight-semibold"
|
||||
@queryParams={{query-params version=list.item.version}}
|
||||
>
|
||||
View version {{list.item.version}}
|
||||
</SecretLink>
|
||||
</li>
|
||||
<li class="action">
|
||||
<SecretLink
|
||||
@mode="edit"
|
||||
@secret={{model.id}}
|
||||
@class="has-text-black has-text-weight-semibold"
|
||||
@queryParams={{query-params version=list.item.version}}
|
||||
>
|
||||
Create new version from {{list.item.version}}
|
||||
</SecretLink>
|
||||
</li>
|
||||
</SecretVersionMenu>
|
||||
</Item.menu>
|
||||
</ListItem>
|
||||
</ListView>
|
||||
1
vendor/github.com/hashicorp/vault-plugin-secrets-azure/Gopkg.lock
generated
vendored
1
vendor/github.com/hashicorp/vault-plugin-secrets-azure/Gopkg.lock
generated
vendored
@@ -427,6 +427,7 @@
|
||||
"github.com/hashicorp/go-multierror",
|
||||
"github.com/hashicorp/go-uuid",
|
||||
"github.com/hashicorp/vault/helper/jsonutil",
|
||||
"github.com/hashicorp/vault/helper/locksutil",
|
||||
"github.com/hashicorp/vault/helper/logging",
|
||||
"github.com/hashicorp/vault/helper/pluginutil",
|
||||
"github.com/hashicorp/vault/helper/useragent",
|
||||
|
||||
7
vendor/github.com/hashicorp/vault-plugin-secrets-azure/backend.go
generated
vendored
7
vendor/github.com/hashicorp/vault-plugin-secrets-azure/backend.go
generated
vendored
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/vault/helper/locksutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
@@ -15,6 +16,10 @@ type azureSecretBackend struct {
|
||||
getProvider func(*clientSettings) (AzureProvider, error)
|
||||
settings *clientSettings
|
||||
lock sync.RWMutex
|
||||
|
||||
// Creating/deleting passwords against a single Application is a PATCH
|
||||
// operation that must be locked per Application Object ID.
|
||||
appLocks []*locksutil.LockEntry
|
||||
}
|
||||
|
||||
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
||||
@@ -44,12 +49,14 @@ func backend() *azureSecretBackend {
|
||||
),
|
||||
Secrets: []*framework.Secret{
|
||||
secretServicePrincipal(&b),
|
||||
secretStaticServicePrincipal(&b),
|
||||
},
|
||||
BackendType: logical.TypeLogical,
|
||||
Invalidate: b.invalidate,
|
||||
}
|
||||
|
||||
b.getProvider = newAzureProvider
|
||||
b.appLocks = locksutil.CreateLocks()
|
||||
|
||||
return &b
|
||||
}
|
||||
|
||||
87
vendor/github.com/hashicorp/vault-plugin-secrets-azure/client.go
generated
vendored
87
vendor/github.com/hashicorp/vault-plugin-secrets-azure/client.go
generated
vendored
@@ -116,7 +116,7 @@ func (c *client) createSP(
|
||||
}
|
||||
|
||||
resultRaw, err := retry(ctx, func() (interface{}, bool, error) {
|
||||
now := time.Now()
|
||||
now := time.Now().UTC()
|
||||
result, err := c.provider.CreateServicePrincipal(ctx, graphrbac.ServicePrincipalCreateParameters{
|
||||
AppID: app.AppID,
|
||||
AccountEnabled: to.BoolPtr(true),
|
||||
@@ -143,6 +143,91 @@ func (c *client) createSP(
|
||||
return &result, password, err
|
||||
}
|
||||
|
||||
// addAppPassword adds a new password to an App's credentials list.
|
||||
func (c *client) addAppPassword(ctx context.Context, appObjID string, duration time.Duration) (string, string, error) {
|
||||
keyID, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Key IDs are not secret, and they're a convenient way for an operator to identify Vault-generated
|
||||
// passwords. These must be UUIDs, so the three leading bytes will be used as an indicator.
|
||||
keyID = "ffffff" + keyID[6:]
|
||||
|
||||
password, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
cred := graphrbac.PasswordCredential{
|
||||
StartDate: &date.Time{Time: now},
|
||||
EndDate: &date.Time{Time: now.Add(duration)},
|
||||
KeyID: to.StringPtr(keyID),
|
||||
Value: to.StringPtr(password),
|
||||
}
|
||||
|
||||
// Load current credentials
|
||||
resp, err := c.provider.ListApplicationPasswordCredentials(ctx, appObjID)
|
||||
if err != nil {
|
||||
return "", "", errwrap.Wrapf("error fetching credentials: {{err}}", err)
|
||||
}
|
||||
curCreds := *resp.Value
|
||||
|
||||
// Add and save credentials
|
||||
curCreds = append(curCreds, cred)
|
||||
|
||||
if _, err := c.provider.UpdateApplicationPasswordCredentials(ctx, appObjID,
|
||||
graphrbac.PasswordCredentialsUpdateParameters{
|
||||
Value: &curCreds,
|
||||
},
|
||||
); err != nil {
|
||||
if strings.Contains(err.Error(), "size of the object has exceeded its limit") {
|
||||
err = errors.New("maximum number of Application passwords reached")
|
||||
}
|
||||
return "", "", errwrap.Wrapf("error updating credentials: {{err}}", err)
|
||||
}
|
||||
|
||||
return keyID, password, nil
|
||||
}
|
||||
|
||||
// deleteAppPassword removes a password, if present, from an App's credentials list.
|
||||
func (c *client) deleteAppPassword(ctx context.Context, appObjID, keyID string) error {
|
||||
// Load current credentials
|
||||
resp, err := c.provider.ListApplicationPasswordCredentials(ctx, appObjID)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("error fetching credentials: {{err}}", err)
|
||||
}
|
||||
curCreds := *resp.Value
|
||||
|
||||
// Remove credential
|
||||
found := false
|
||||
for i := range curCreds {
|
||||
if to.String(curCreds[i].KeyID) == keyID {
|
||||
curCreds[i] = curCreds[len(curCreds)-1]
|
||||
curCreds = curCreds[:len(curCreds)-1]
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// KeyID is not present, so nothing to do
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save new credentials list
|
||||
if _, err := c.provider.UpdateApplicationPasswordCredentials(ctx, appObjID,
|
||||
graphrbac.PasswordCredentialsUpdateParameters{
|
||||
Value: &curCreds,
|
||||
},
|
||||
); err != nil {
|
||||
return errwrap.Wrapf("error updating credentials: {{err}}", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteApp deletes an Azure application.
|
||||
func (c *client) deleteApp(ctx context.Context, appObjectID string) error {
|
||||
resp, err := c.provider.DeleteApplication(ctx, appObjectID)
|
||||
|
||||
95
vendor/github.com/hashicorp/vault-plugin-secrets-azure/path_roles.go
generated
vendored
95
vendor/github.com/hashicorp/vault-plugin-secrets-azure/path_roles.go
generated
vendored
@@ -21,12 +21,14 @@ const (
|
||||
credentialTypeSP = 0
|
||||
)
|
||||
|
||||
// Role is a Vault role construct that maps to Azure roles
|
||||
// Role is a Vault role construct that maps to Azure roles or Applications
|
||||
type Role struct {
|
||||
CredentialType int `json:"credential_type"` // Reserved. Always SP at this time.
|
||||
AzureRoles []*azureRole `json:"azure_roles"`
|
||||
TTL time.Duration `json:"ttl"`
|
||||
MaxTTL time.Duration `json:"max_ttl"`
|
||||
CredentialType int `json:"credential_type"` // Reserved. Always SP at this time.
|
||||
AzureRoles []*azureRole `json:"azure_roles"`
|
||||
ApplicationID string `json:"application_id"`
|
||||
ApplicationObjectID string `json:"application_object_id"`
|
||||
TTL time.Duration `json:"ttl"`
|
||||
MaxTTL time.Duration `json:"max_ttl"`
|
||||
}
|
||||
|
||||
// azureRole is an Azure Role (https://docs.microsoft.com/en-us/azure/role-based-access-control/overview) applied
|
||||
@@ -45,11 +47,15 @@ func pathsRole(b *azureSecretBackend) []*framework.Path {
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeLowerCaseString,
|
||||
Description: "Name of the role",
|
||||
Description: "Name of the role.",
|
||||
},
|
||||
"application_object_id": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Application Object ID to use for static service principal credentials.",
|
||||
},
|
||||
"azure_roles": {
|
||||
Type: framework.TypeString,
|
||||
Description: "JSON list of Azure roles to assign",
|
||||
Description: "JSON list of Azure roles to assign.",
|
||||
},
|
||||
"ttl": {
|
||||
Type: framework.TypeDurationSecond,
|
||||
@@ -85,12 +91,24 @@ func pathsRole(b *azureSecretBackend) []*framework.Path {
|
||||
// pathRoleUpdate creates or updates Vault roles.
|
||||
//
|
||||
// Basic validity check are made to verify that the provided fields meet requirements
|
||||
// and the Azure roles exist. The Azure role lookup step will all the operator to provide
|
||||
// a role name or ID. ID is unambigious and will be used if provided. Given just role name,
|
||||
// a search will be performed and if exactly one match is found, that role will be used.
|
||||
// for the given credential type.
|
||||
//
|
||||
// Dynamic Service Principal:
|
||||
// Azure roles are checked for existence. The Azure role lookup step will allow the
|
||||
// operator to provide a role name or ID. ID is unambigious and will be used if provided.
|
||||
// Given just role name, a search will be performed and if exactly one match is found,
|
||||
// that role will be used.
|
||||
//
|
||||
// Static Service Principal:
|
||||
// The provided Application Object ID is checked for existence.
|
||||
func (b *azureSecretBackend) pathRoleUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
var resp *logical.Response
|
||||
|
||||
client, err := b.getClient(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// load or create role
|
||||
name := d.Get("name").(string)
|
||||
role, err := getRole(ctx, name, req.Storage)
|
||||
@@ -107,7 +125,7 @@ func (b *azureSecretBackend) pathRoleUpdate(ctx context.Context, req *logical.Re
|
||||
}
|
||||
}
|
||||
|
||||
// update role with any provided parameters
|
||||
// load and validate TTLs
|
||||
if ttlRaw, ok := d.GetOk("ttl"); ok {
|
||||
role.TTL = time.Duration(ttlRaw.(int)) * time.Second
|
||||
} else if req.Operation == logical.CreateOperation {
|
||||
@@ -120,6 +138,24 @@ func (b *azureSecretBackend) pathRoleUpdate(ctx context.Context, req *logical.Re
|
||||
role.MaxTTL = time.Duration(d.Get("max_ttl").(int)) * time.Second
|
||||
}
|
||||
|
||||
if role.MaxTTL != 0 && role.TTL > role.MaxTTL {
|
||||
return logical.ErrorResponse("ttl cannot be greater than max_ttl"), nil
|
||||
}
|
||||
|
||||
// update and verify Application Object ID if provided
|
||||
if appObjectID, ok := d.GetOk("application_object_id"); ok {
|
||||
role.ApplicationObjectID = appObjectID.(string)
|
||||
}
|
||||
|
||||
if role.ApplicationObjectID != "" {
|
||||
app, err := client.provider.GetApplication(ctx, role.ApplicationObjectID)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("error loading Application: {{err}}", err)
|
||||
}
|
||||
role.ApplicationID = to.String(app.AppID)
|
||||
}
|
||||
|
||||
// update and verify Azure roles, including looking up each role by ID or name.
|
||||
if roles, ok := d.GetOk("azure_roles"); ok {
|
||||
parsedRoles := make([]*azureRole, 0) // non-nil to avoid a "missing roles" error later
|
||||
|
||||
@@ -130,18 +166,11 @@ func (b *azureSecretBackend) pathRoleUpdate(ctx context.Context, req *logical.Re
|
||||
role.AzureRoles = parsedRoles
|
||||
}
|
||||
|
||||
// verify Azure roles, including looking up each role
|
||||
// by ID or name.
|
||||
c, err := b.getClient(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleIDs := make(map[string]bool)
|
||||
for _, r := range role.AzureRoles {
|
||||
var roleDef authorization.RoleDefinition
|
||||
if r.RoleID != "" {
|
||||
roleDef, err = c.provider.GetRoleByID(ctx, r.RoleID)
|
||||
roleDef, err = client.provider.GetRoleByID(ctx, r.RoleID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "RoleDefinitionDoesNotExist") {
|
||||
return logical.ErrorResponse(fmt.Sprintf("no role found for role_id: '%s'", r.RoleID)), nil
|
||||
@@ -149,7 +178,7 @@ func (b *azureSecretBackend) pathRoleUpdate(ctx context.Context, req *logical.Re
|
||||
return nil, errwrap.Wrapf("unable to lookup Azure role: {{err}}", err)
|
||||
}
|
||||
} else {
|
||||
defs, err := c.findRoles(ctx, r.RoleName)
|
||||
defs, err := client.findRoles(ctx, r.RoleName)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("unable to lookup Azure role: {{err}}", err)
|
||||
}
|
||||
@@ -171,13 +200,8 @@ func (b *azureSecretBackend) pathRoleUpdate(ctx context.Context, req *logical.Re
|
||||
r.RoleName, r.RoleID = roleDefName, roleDefID
|
||||
}
|
||||
|
||||
// validate role definition constraints
|
||||
if role.MaxTTL != 0 && role.TTL > role.MaxTTL {
|
||||
return logical.ErrorResponse("ttl cannot be greater than max_ttl"), nil
|
||||
}
|
||||
|
||||
if len(role.AzureRoles) == 0 {
|
||||
return logical.ErrorResponse("missing Azure role definitions"), nil
|
||||
if role.ApplicationObjectID == "" && len(role.AzureRoles) == 0 {
|
||||
return logical.ErrorResponse("either Azure role definitions or an Application Object ID must be provided"), nil
|
||||
}
|
||||
|
||||
// save role
|
||||
@@ -206,6 +230,7 @@ func (b *azureSecretBackend) pathRoleRead(ctx context.Context, req *logical.Requ
|
||||
data["ttl"] = r.TTL / time.Second
|
||||
data["max_ttl"] = r.MaxTTL / time.Second
|
||||
data["azure_roles"] = r.AzureRoles
|
||||
data["application_object_id"] = r.ApplicationObjectID
|
||||
|
||||
return &logical.Response{
|
||||
Data: data,
|
||||
@@ -272,17 +297,19 @@ func getRole(ctx context.Context, name string, s logical.Storage) (*Role, error)
|
||||
const roleHelpSyn = "Manage the Vault roles used to generate Azure credentials."
|
||||
const roleHelpDesc = `
|
||||
This path allows you to read and write roles that are used to generate Azure login
|
||||
credentials. These roles are associated with Azure roles, which are in turn used to
|
||||
control permissions to Azure resources.
|
||||
credentials. These roles are associated with either an existing Application, or a set
|
||||
of Azure roles, which are used to control permissions to Azure resources.
|
||||
|
||||
If the backend is mounted at "azure", you would create a Vault role at "azure/roles/my_role",
|
||||
and request credentials from "azure/creds/my_role".
|
||||
|
||||
Each Vault role is configured with the standard ttl parameters and a list of Azure
|
||||
roles and scopes. These Azure roles will be fetched during the Vault role creation
|
||||
and must exist for the request to succeed. Multiple Azure roles may be specified. When
|
||||
a used requests credentials against the Vault role, and new service principal is created
|
||||
and the configured set of Azure roles are assigned to it.
|
||||
Each Vault role is configured with the standard ttl parameters and either a list of Azure
|
||||
roles and scopes, or an Application Object ID. Any Azure roles will be fetched during the
|
||||
Vault role creation and must exist for the request to succeed. Similarly, the Application
|
||||
Object ID will be verified if provided. When a used requests credentials against the Vault
|
||||
role, a new password will be created for the Application if an Application Object ID was
|
||||
configured. Otherwise, a new service principal will be created and the configured set of
|
||||
Azure roles are assigned to it.
|
||||
`
|
||||
const roleListHelpSyn = `List existing roles.`
|
||||
const roleListHelpDesc = `List existing roles by name.`
|
||||
|
||||
122
vendor/github.com/hashicorp/vault-plugin-secrets-azure/path_service_principal.go
generated
vendored
122
vendor/github.com/hashicorp/vault-plugin-secrets-azure/path_service_principal.go
generated
vendored
@@ -8,14 +8,19 @@ import (
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/to"
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/vault/helper/locksutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
const (
|
||||
SecretTypeSP = "service_principal"
|
||||
SecretTypeSP = "service_principal"
|
||||
SecretTypeStaticSP = "static_service_principal"
|
||||
)
|
||||
|
||||
// SPs will be created with a far-future expiration in Azure
|
||||
var spExpiration = 10 * 365 * 24 * time.Hour
|
||||
|
||||
func secretServicePrincipal(b *azureSecretBackend) *framework.Secret {
|
||||
return &framework.Secret{
|
||||
Type: SecretTypeSP,
|
||||
@@ -24,6 +29,14 @@ func secretServicePrincipal(b *azureSecretBackend) *framework.Secret {
|
||||
}
|
||||
}
|
||||
|
||||
func secretStaticServicePrincipal(b *azureSecretBackend) *framework.Secret {
|
||||
return &framework.Secret{
|
||||
Type: SecretTypeStaticSP,
|
||||
Renew: b.spRenew,
|
||||
Revoke: b.staticSPRevoke,
|
||||
}
|
||||
}
|
||||
|
||||
func pathServicePrincipal(b *azureSecretBackend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: fmt.Sprintf("creds/%s", framework.GenericNameRegex("role")),
|
||||
@@ -41,19 +54,15 @@ func pathServicePrincipal(b *azureSecretBackend) *framework.Path {
|
||||
}
|
||||
}
|
||||
|
||||
// pathSPRead generates Azure an service principal and credentials.
|
||||
//
|
||||
// This is a multistep process of:
|
||||
// 1. Create an Azure application
|
||||
// 2. Create a service principal associated with the new App
|
||||
// 3. Assign roles
|
||||
// pathSPRead generates Azure credentials based on the role credential type.
|
||||
func (b *azureSecretBackend) pathSPRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
c, err := b.getClient(ctx, req.Storage)
|
||||
client, err := b.getClient(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleName := d.Get("role").(string)
|
||||
|
||||
role, err := getRole(ctx, roleName, req.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -63,6 +72,26 @@ func (b *azureSecretBackend) pathSPRead(ctx context.Context, req *logical.Reques
|
||||
return logical.ErrorResponse(fmt.Sprintf("role '%s' does not exists", roleName)), nil
|
||||
}
|
||||
|
||||
var resp *logical.Response
|
||||
|
||||
if role.ApplicationObjectID != "" {
|
||||
resp, err = b.createStaticSPSecret(ctx, client, roleName, role)
|
||||
} else {
|
||||
resp, err = b.createSPSecret(ctx, client, roleName, role)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Secret.TTL = role.TTL
|
||||
resp.Secret.MaxTTL = role.MaxTTL
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// createSPSecret generates a new App/Service Principal.
|
||||
func (b *azureSecretBackend) createSPSecret(ctx context.Context, c *client, roleName string, role *Role) (*logical.Response, error) {
|
||||
// Create the App, which is the top level object to be tracked in the secret
|
||||
// and deleted upon revocation. If any subsequent step fails, the App is deleted.
|
||||
app, err := c.createApp(ctx)
|
||||
@@ -72,38 +101,61 @@ func (b *azureSecretBackend) pathSPRead(ctx context.Context, req *logical.Reques
|
||||
appID := to.String(app.AppID)
|
||||
appObjID := to.String(app.ObjectID)
|
||||
|
||||
// Create the SP. A far future credential expiration is set on the Azure side.
|
||||
sp, password, err := c.createSP(ctx, app, 10*365*24*time.Hour)
|
||||
// Create a service principal associated with the new App
|
||||
sp, password, err := c.createSP(ctx, app, spExpiration)
|
||||
if err != nil {
|
||||
c.deleteApp(ctx, appObjID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Assign Azure roles to the new SP
|
||||
raIDs, err := c.assignRoles(ctx, sp, role.AzureRoles)
|
||||
if err != nil {
|
||||
c.deleteApp(ctx, appObjID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := b.Secret(SecretTypeSP).Response(map[string]interface{}{
|
||||
data := map[string]interface{}{
|
||||
"client_id": appID,
|
||||
"client_secret": password,
|
||||
}, map[string]interface{}{
|
||||
}
|
||||
internalData := map[string]interface{}{
|
||||
"app_object_id": appObjID,
|
||||
"role_assignment_ids": raIDs,
|
||||
"role": roleName,
|
||||
})
|
||||
}
|
||||
|
||||
resp.Secret.TTL = role.TTL
|
||||
resp.Secret.MaxTTL = role.MaxTTL
|
||||
return b.Secret(SecretTypeSP).Response(data, internalData), nil
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
// createStaticSPSecret adds a new password to the App associated with the role.
|
||||
func (b *azureSecretBackend) createStaticSPSecret(ctx context.Context, c *client, roleName string, role *Role) (*logical.Response, error) {
|
||||
lock := locksutil.LockForKey(b.appLocks, role.ApplicationObjectID)
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
keyID, password, err := c.addAppPassword(ctx, role.ApplicationObjectID, spExpiration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"client_id": role.ApplicationID,
|
||||
"client_secret": password,
|
||||
}
|
||||
internalData := map[string]interface{}{
|
||||
"app_object_id": role.ApplicationObjectID,
|
||||
"key_id": keyID,
|
||||
"role": roleName,
|
||||
}
|
||||
|
||||
return b.Secret(SecretTypeStaticSP).Response(data, internalData), nil
|
||||
}
|
||||
|
||||
func (b *azureSecretBackend) spRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
roleRaw, ok := req.Secret.InternalData["role"]
|
||||
if !ok {
|
||||
return nil, errors.New("internal data not found")
|
||||
return nil, errors.New("internal data 'role' not found")
|
||||
}
|
||||
|
||||
role, err := getRole(ctx, roleRaw.(string), req.Storage)
|
||||
@@ -127,7 +179,7 @@ func (b *azureSecretBackend) spRevoke(ctx context.Context, req *logical.Request,
|
||||
|
||||
appObjectIDRaw, ok := req.Secret.InternalData["app_object_id"]
|
||||
if !ok {
|
||||
return nil, errors.New("internal data not found")
|
||||
return nil, errors.New("internal data 'app_object_id' not found")
|
||||
}
|
||||
|
||||
appObjectID := appObjectIDRaw.(string)
|
||||
@@ -155,12 +207,38 @@ func (b *azureSecretBackend) spRevoke(ctx context.Context, req *logical.Request,
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (b *azureSecretBackend) staticSPRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
appObjectIDRaw, ok := req.Secret.InternalData["app_object_id"]
|
||||
if !ok {
|
||||
return nil, errors.New("internal data 'app_object_id' not found")
|
||||
}
|
||||
|
||||
appObjectID := appObjectIDRaw.(string)
|
||||
|
||||
c, err := b.getClient(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("error during revoke: {{err}}", err)
|
||||
}
|
||||
|
||||
keyIDRaw, ok := req.Secret.InternalData["key_id"]
|
||||
if !ok {
|
||||
return nil, errors.New("internal data 'key_id' not found")
|
||||
}
|
||||
|
||||
lock := locksutil.LockForKey(b.appLocks, appObjectID)
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return nil, c.deleteAppPassword(ctx, appObjectID, keyIDRaw.(string))
|
||||
}
|
||||
|
||||
const pathServicePrincipalHelpSyn = `
|
||||
Request Service Principal credentials for a given Vault role.
|
||||
`
|
||||
|
||||
const pathServicePrincipalHelpDesc = `
|
||||
This path creates a Service Principal and assigns Azure roles for a
|
||||
given Vault role, returning the associated login credentials. The
|
||||
Service Principal will be automatically deleted when the lease has expired.
|
||||
This path creates or updates dynamic Service Principal credentials.
|
||||
The associated role can be configured to create a new App/Service Principal,
|
||||
or add a new password to an existing App. The Service Principal or password
|
||||
will be automatically deleted when the lease has expired.
|
||||
`
|
||||
|
||||
18
vendor/github.com/hashicorp/vault-plugin-secrets-azure/provider.go
generated
vendored
18
vendor/github.com/hashicorp/vault-plugin-secrets-azure/provider.go
generated
vendored
@@ -23,6 +23,12 @@ type AzureProvider interface {
|
||||
type ApplicationsClient interface {
|
||||
CreateApplication(ctx context.Context, parameters graphrbac.ApplicationCreateParameters) (graphrbac.Application, error)
|
||||
DeleteApplication(ctx context.Context, applicationObjectID string) (autorest.Response, error)
|
||||
GetApplication(ctx context.Context, applicationObjectID string) (graphrbac.Application, error)
|
||||
UpdateApplicationPasswordCredentials(
|
||||
ctx context.Context,
|
||||
applicationObjectID string,
|
||||
parameters graphrbac.PasswordCredentialsUpdateParameters) (result autorest.Response, err error)
|
||||
ListApplicationPasswordCredentials(ctx context.Context, applicationObjectID string) (result graphrbac.PasswordCredentialListResult, err error)
|
||||
}
|
||||
|
||||
type ServicePrincipalsClient interface {
|
||||
@@ -133,12 +139,24 @@ func (p *provider) CreateApplication(ctx context.Context, parameters graphrbac.A
|
||||
return p.appClient.Create(ctx, parameters)
|
||||
}
|
||||
|
||||
func (p *provider) GetApplication(ctx context.Context, applicationObjectID string) (graphrbac.Application, error) {
|
||||
return p.appClient.Get(ctx, applicationObjectID)
|
||||
}
|
||||
|
||||
// DeleteApplication deletes an Azure application object.
|
||||
// This will in turn remove the service principal (but not the role assignments).
|
||||
func (p *provider) DeleteApplication(ctx context.Context, applicationObjectID string) (autorest.Response, error) {
|
||||
return p.appClient.Delete(ctx, applicationObjectID)
|
||||
}
|
||||
|
||||
func (p *provider) UpdateApplicationPasswordCredentials(ctx context.Context, applicationObjectID string, parameters graphrbac.PasswordCredentialsUpdateParameters) (result autorest.Response, err error) {
|
||||
return p.appClient.UpdatePasswordCredentials(ctx, applicationObjectID, parameters)
|
||||
}
|
||||
|
||||
func (p *provider) ListApplicationPasswordCredentials(ctx context.Context, applicationObjectID string) (result graphrbac.PasswordCredentialListResult, err error) {
|
||||
return p.appClient.ListPasswordCredentials(ctx, applicationObjectID)
|
||||
}
|
||||
|
||||
// CreateServicePrincipal creates a new Azure service principal.
|
||||
// An Application must be created prior to calling this and pass in parameters.
|
||||
func (p *provider) CreateServicePrincipal(ctx context.Context, parameters graphrbac.ServicePrincipalCreateParameters) (graphrbac.ServicePrincipal, error) {
|
||||
|
||||
6
vendor/vendor.json
vendored
6
vendor/vendor.json
vendored
@@ -1449,10 +1449,10 @@
|
||||
"revisionTime": "2018-10-03T22:47:18Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "6BinFSaXH5g1SSgNSOZRIPbmWkc=",
|
||||
"checksumSHA1": "Zll08//LoV7ZR+GIZybAjkflkfQ=",
|
||||
"path": "github.com/hashicorp/vault-plugin-secrets-azure",
|
||||
"revision": "01e3797517d6b30e909825fb7c08a7cc1d97809c",
|
||||
"revisionTime": "2018-10-04T17:44:28Z"
|
||||
"revision": "3dfddbec9f0648b7fc688c649580e6a1e110e61a",
|
||||
"revisionTime": "2018-10-17T18:44:37Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "tFP1EEyVlomSSx46NHDZWGPzUz0=",
|
||||
|
||||
@@ -185,7 +185,7 @@ can create new passwords for this service principal. Any changes to the service
|
||||
permissions affect all clients. Furthermore, Azure does not provide any logging with
|
||||
regard to _which_ credential was used for an operation.
|
||||
|
||||
An important limitation when using an existing service princicpal is that Azure limits the
|
||||
An important limitation when using an existing service principal is that Azure limits the
|
||||
number of passwords for a single Application. This limit is based on Application object
|
||||
size and isn't firmly specified, but in practice hundreds of passwords can be issued per
|
||||
Application. An error will be returned if the object size is reached. This limit can be
|
||||
|
||||
@@ -106,9 +106,9 @@
|
||||
|
||||
/guides/getting-started/index.html https://learn.hashicorp.com/vault
|
||||
|
||||
/guides/operations/index.html https://learn.hashicorp.com/vault/vault/?track=operations#operations
|
||||
/guides/operations/reference-architecture.html https://learn.hashicorp.com/vault/vault/operations/ops-reference-architecture
|
||||
/guides/operations/deployment-guide.html https://learn.hashicorp.com/vault/vault/operations/ops-deployment-guide
|
||||
/guides/operations/index.html https://learn.hashicorp.com/vault/?track=operations#operations
|
||||
/guides/operations/reference-architecture.html https://learn.hashicorp.com/vault/operations/ops-reference-architecture
|
||||
/guides/operations/deployment-guide.html https://learn.hashicorp.com/vault/operations/ops-deployment-guide
|
||||
/guides/operations/vault-ha-consul.html https://learn.hashicorp.com/vault/operations/ops-vault-ha-consul
|
||||
/guides/operations/production.html https://learn.hashicorp.com/vault/operations/production-hardening
|
||||
/guides/operations/generate-root.html https://learn.hashicorp.com/vault/operations/ops-generate-root
|
||||
@@ -164,3 +164,4 @@
|
||||
/intro/vs/keywhiz.html /docs/vs/keywhiz
|
||||
/intro/vs/kms.html /docs/vs/kms
|
||||
/intro/what-is-vault/index.html /docs/what-is-vault
|
||||
/intro/getting-started/install.html https://learn.hashicorp.com/vault/getting-started/install
|
||||
|
||||
Reference in New Issue
Block a user