Merge branch 'oss-master' into 1.0-beta-oss

This commit is contained in:
Matthew Irish
2018-10-19 20:40:36 -05:00
37 changed files with 779 additions and 341 deletions

View File

@@ -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

View File

@@ -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")

View File

@@ -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,

View File

@@ -127,6 +127,7 @@ func TestPlugin_NetRPC_Main(t *testing.T) {
return
}
os.Unsetenv(pluginutil.PluginVaultVersionEnv)
p := &mockPlugin{
users: make(map[string][]string),
}

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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{} {

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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:

View File

@@ -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",
}

View File

@@ -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');

View File

@@ -2,4 +2,6 @@ import Component from '@ember/component';
export default Component.extend({
tagName: '',
item: null,
hasMenu: null,
});

View File

@@ -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();
},

View 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);
},
},
});

View File

@@ -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' });

View File

@@ -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(

View File

@@ -0,0 +1 @@
export { default } from './version';

View 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 });
},
});

View File

@@ -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];
},

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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}}

View File

@@ -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>

View 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>

View File

@@ -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">

View 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>

View File

@@ -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",

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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.`

View File

@@ -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.
`

View File

@@ -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
View File

@@ -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=",

View File

@@ -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

View File

@@ -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