diff --git a/api/sys_mounts.go b/api/sys_mounts.go index 52f51139f77b6..665376f28dbf9 100644 --- a/api/sys_mounts.go +++ b/api/sys_mounts.go @@ -247,6 +247,7 @@ type MountInput struct { SealWrap bool `json:"seal_wrap" mapstructure:"seal_wrap"` ExternalEntropyAccess bool `json:"external_entropy_access" mapstructure:"external_entropy_access"` Options map[string]string `json:"options"` + Version string `json:"version,omitempty"` // Deprecated: Newer server responses should be returning this information in the // Type field (json: "type") instead. @@ -281,6 +282,10 @@ type MountOutput struct { Local bool `json:"local"` SealWrap bool `json:"seal_wrap" mapstructure:"seal_wrap"` ExternalEntropyAccess bool `json:"external_entropy_access" mapstructure:"external_entropy_access"` + Version string `json:"version"` + RunningVersion string `json:"running_version"` + Sha string `json:"sha"` + RunningSha string `json:"running_sha"` } type MountConfigOutput struct { diff --git a/api/sys_mounts_test.go b/api/sys_mounts_test.go new file mode 100644 index 0000000000000..5b3540e04d2ff --- /dev/null +++ b/api/sys_mounts_test.go @@ -0,0 +1,153 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestListMounts(t *testing.T) { + mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultMountsHandler)) + defer mockVaultServer.Close() + + cfg := DefaultConfig() + cfg.Address = mockVaultServer.URL + client, err := NewClient(cfg) + if err != nil { + t.Fatal(err) + } + + resp, err := client.Sys().ListMounts() + if err != nil { + t.Fatal(err) + } + + expectedMounts := map[string]struct { + Type string + Version string + }{ + "cubbyhole/": {Type: "cubbyhole", Version: "v1.0.0"}, + "identity/": {Type: "identity", Version: ""}, + "secret/": {Type: "kv", Version: ""}, + "sys/": {Type: "system", Version: ""}, + } + + for path, mount := range resp { + expected, ok := expectedMounts[path] + if !ok { + t.Errorf("Unexpected mount: %s: %+v", path, mount) + continue + } + if expected.Type != mount.Type || expected.Version != mount.Version { + t.Errorf("Mount did not match: %s -> expected %+v but got %+v", path, expected, mount) + } + } + + for path, expected := range expectedMounts { + mount, ok := resp[path] + if !ok { + t.Errorf("Expected mount not found mount: %s: %+v", path, expected) + continue + } + if expected.Type != mount.Type || expected.Version != mount.Version { + t.Errorf("Mount did not match: %s -> expected %+v but got %+v", path, expected, mount) + } + } +} + +func mockVaultMountsHandler(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(listMountsResponse)) +} + +const listMountsResponse = `{ + "request_id": "3cd881e9-ea50-2e06-90b2-5641667485fa", + "lease_id": "", + "lease_duration": 0, + "renewable": false, + "data": { + "cubbyhole/": { + "accessor": "cubbyhole_2e3fc28d", + "config": { + "default_lease_ttl": 0, + "force_no_cache": false, + "max_lease_ttl": 0 + }, + "description": "per-token private secret storage", + "external_entropy_access": false, + "local": true, + "options": null, + "running_sha": "", + "running_version": "", + "seal_wrap": false, + "sha": "", + "type": "cubbyhole", + "uuid": "575063dc-5ef8-4487-c842-22c494c19a6f", + "version": "v1.0.0" + }, + "identity/": { + "accessor": "identity_6e01c327", + "config": { + "default_lease_ttl": 0, + "force_no_cache": false, + "max_lease_ttl": 0, + "passthrough_request_headers": [ + "Authorization" + ] + }, + "description": "identity store", + "external_entropy_access": false, + "local": false, + "options": null, + "running_sha": "", + "running_version": "", + "seal_wrap": false, + "sha": "", + "type": "identity", + "uuid": "187d7eba-3471-554b-c2d9-1479612c8046", + "version": "" + }, + "secret/": { + "accessor": "kv_3e2f282f", + "config": { + "default_lease_ttl": 0, + "force_no_cache": false, + "max_lease_ttl": 0 + }, + "description": "key/value secret storage", + "external_entropy_access": false, + "local": false, + "options": { + "version": "2" + }, + "running_sha": "", + "running_version": "", + "seal_wrap": false, + "sha": "", + "type": "kv", + "uuid": "13375e0f-876e-7e96-0a3e-076f37b6b69d", + "version": "" + }, + "sys/": { + "accessor": "system_93503264", + "config": { + "default_lease_ttl": 0, + "force_no_cache": false, + "max_lease_ttl": 0, + "passthrough_request_headers": [ + "Accept" + ] + }, + "description": "system endpoints used for control, policy and debugging", + "external_entropy_access": false, + "local": false, + "options": null, + "running_sha": "", + "running_version": "", + "seal_wrap": true, + "sha": "", + "type": "system", + "uuid": "1373242d-cc4d-c023-410b-7f336e7ba0a8", + "version": "" + } + } +}` diff --git a/api/sys_plugins.go b/api/sys_plugins.go index 004ee222bfdf8..118134380d5b6 100644 --- a/api/sys_plugins.go +++ b/api/sys_plugins.go @@ -22,6 +22,8 @@ type ListPluginsResponse struct { // PluginsByType is the list of plugins by type. PluginsByType map[consts.PluginType][]string `json:"types"` + Details []PluginDetails `json:"details,omitempty"` + // Names is the list of names of the plugins. // // Deprecated: Newer server responses should be returning PluginsByType (json: @@ -29,6 +31,13 @@ type ListPluginsResponse struct { Names []string `json:"names"` } +type PluginDetails struct { + Type string `json:"string"` + Name string `json:"name"` + Version string `json:"version,omitempty"` + Builtin bool `json:"builtin"` +} + // ListPlugins wraps ListPluginsWithContext using context.Background. func (c *Sys) ListPlugins(i *ListPluginsInput) (*ListPluginsResponse, error) { return c.ListPluginsWithContext(context.Background(), i) @@ -98,6 +107,7 @@ func (c *Sys) ListPluginsWithContext(ctx context.Context, i *ListPluginsInput) ( result := &ListPluginsResponse{ PluginsByType: make(map[consts.PluginType][]string), + Details: []PluginDetails{}, } if i.Type == consts.PluginTypeUnknown { for _, pluginType := range consts.PluginTypes { @@ -129,6 +139,12 @@ func (c *Sys) ListPluginsWithContext(ctx context.Context, i *ListPluginsInput) ( result.PluginsByType[i.Type] = respKeys } + if detailed, ok := secret.Data["detailed"]; ok { + if err := mapstructure.Decode(detailed, &result.Details); err != nil { + return nil, err + } + } + return result, nil } @@ -194,6 +210,9 @@ type RegisterPluginInput struct { // SHA256 is the shasum of the plugin. SHA256 string `json:"sha256,omitempty"` + + // Version is the optional version of the plugin being registered + Version string `json:"version,omitempty"` } // RegisterPlugin wraps RegisterPluginWithContext using context.Background. @@ -227,6 +246,9 @@ type DeregisterPluginInput struct { // Type of the plugin. Required. Type consts.PluginType `json:"type"` + + // Version of the plugin. Optional. + Version string `json:"version,omitempty"` } // DeregisterPlugin wraps DeregisterPluginWithContext using context.Background. @@ -242,7 +264,7 @@ func (c *Sys) DeregisterPluginWithContext(ctx context.Context, i *DeregisterPlug path := catalogPathByType(i.Type, i.Name) req := c.c.NewRequest(http.MethodDelete, path) - + req.Params.Set("version", i.Version) resp, err := c.c.rawRequestWithContext(ctx, req) if err == nil { defer resp.Body.Close() diff --git a/api/sys_plugins_test.go b/api/sys_plugins_test.go index 24295b6f22b61..d4a577bac9de8 100644 --- a/api/sys_plugins_test.go +++ b/api/sys_plugins_test.go @@ -9,8 +9,27 @@ import ( "github.com/hashicorp/vault/sdk/helper/consts" ) +func TestRegisterPlugin(t *testing.T) { + mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultHandlerRegister)) + defer mockVaultServer.Close() + + cfg := DefaultConfig() + cfg.Address = mockVaultServer.URL + client, err := NewClient(cfg) + if err != nil { + t.Fatal(err) + } + + err = client.Sys().RegisterPluginWithContext(context.Background(), &RegisterPluginInput{ + Version: "v1.0.0", + }) + if err != nil { + t.Fatal(err) + } +} + func TestListPlugins(t *testing.T) { - mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultHandler)) + mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultHandlerList)) defer mockVaultServer.Close() cfg := DefaultConfig() @@ -44,7 +63,7 @@ func TestListPlugins(t *testing.T) { } } -func mockVaultHandler(w http.ResponseWriter, _ *http.Request) { +func mockVaultHandlerList(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(listUntypedResponse)) } @@ -77,3 +96,9 @@ const listUntypedResponse = `{ "warnings": null, "auth": null }` + +func mockVaultHandlerRegister(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(registerResponse)) +} + +const registerResponse = `{}` diff --git a/builtin/plugin/backend.go b/builtin/plugin/backend.go index b9dd409b2aafd..cb94d3e0cfe20 100644 --- a/builtin/plugin/backend.go +++ b/builtin/plugin/backend.go @@ -62,11 +62,12 @@ func Backend(ctx context.Context, conf *logical.BackendConfig) (*PluginBackend, if err != nil { return nil, err } + version := conf.Config["plugin_version"] sys := conf.System - // NewBackend with isMetadataMode set to true - raw, err := bplugin.NewBackend(ctx, name, pluginType, sys, conf, true) + // NewBackendWithVersion with isMetadataMode set to true + raw, err := bplugin.NewBackendWithVersion(ctx, name, pluginType, sys, conf, true, version) if err != nil { return nil, err } @@ -119,7 +120,7 @@ func (b *PluginBackend) startBackend(ctx context.Context, storage logical.Storag // Ensure proper cleanup of the backend (i.e. call client.Kill()) b.Backend.Cleanup(ctx) - nb, err := bplugin.NewBackend(ctx, pluginName, pluginType, b.config.System, b.config, false) + nb, err := bplugin.NewBackendWithVersion(ctx, pluginName, pluginType, b.config.System, b.config, false, b.config.Config["plugin_version"]) if err != nil { return err } diff --git a/builtin/plugin/backend_lazyLoad_test.go b/builtin/plugin/backend_lazyLoad_test.go index 53c6f9611829d..3d870a7a4209b 100644 --- a/builtin/plugin/backend_lazyLoad_test.go +++ b/builtin/plugin/backend_lazyLoad_test.go @@ -183,3 +183,13 @@ func (v testSystemView) LookupPlugin(context.Context, string, consts.PluginType) }, }, nil } + +func (v testSystemView) LookupPluginVersion(context.Context, string, consts.PluginType, string) (*pluginutil.PluginRunner, error) { + return &pluginutil.PluginRunner{ + Name: "test-plugin-runner", + Builtin: true, + BuiltinFactory: func() (interface{}, error) { + return v.factory, nil + }, + }, nil +} diff --git a/changelog/16856.txt b/changelog/16856.txt new file mode 100644 index 0000000000000..512dd67a76b1c --- /dev/null +++ b/changelog/16856.txt @@ -0,0 +1,3 @@ +```release-note:change +plugins: Add plugin version to auth register, list, and mount table +``` diff --git a/command/auth_enable.go b/command/auth_enable.go index a23c7989f7a13..368ae17d0187a 100644 --- a/command/auth_enable.go +++ b/command/auth_enable.go @@ -37,6 +37,7 @@ type AuthEnableCommand struct { flagExternalEntropyAccess bool flagTokenType string flagVersion int + flagPluginVersion string } func (c *AuthEnableCommand) Synopsis() string { @@ -199,6 +200,13 @@ func (c *AuthEnableCommand) Flags() *FlagSets { Usage: "Select the version of the auth method to run. Not supported by all auth methods.", }) + f.StringVar(&StringVar{ + Name: "plugin-version", + Target: &c.flagPluginVersion, + Default: "", + Usage: "Select the version of the plugin to enable.", + }) + return set } @@ -262,6 +270,7 @@ func (c *AuthEnableCommand) Run(args []string) int { authOpts := &api.EnableAuthOptions{ Type: authType, + Version: c.flagPluginVersion, Description: c.flagDescription, Local: c.flagLocal, SealWrap: c.flagSealWrap, diff --git a/command/auth_list.go b/command/auth_list.go index 5dc29a8282c83..8da66309b0355 100644 --- a/command/auth_list.go +++ b/command/auth_list.go @@ -118,10 +118,10 @@ func (c *AuthListCommand) simpleMounts(auths map[string]*api.AuthMount) []string } sort.Strings(paths) - out := []string{"Path | Type | Accessor | Description"} + out := []string{"Path | Type | Accessor | Description | Version"} for _, path := range paths { mount := auths[path] - out = append(out, fmt.Sprintf("%s | %s | %s | %s", path, mount.Type, mount.Accessor, mount.Description)) + out = append(out, fmt.Sprintf("%s | %s | %s | %s | %s", path, mount.Type, mount.Accessor, mount.Description, mount.Version)) } return out @@ -145,7 +145,7 @@ func (c *AuthListCommand) detailedMounts(auths map[string]*api.AuthMount) []stri } } - out := []string{"Path | Plugin | Accessor | Default TTL | Max TTL | Token Type | Replication | Seal Wrap | External Entropy Access | Options | Description | UUID"} + out := []string{"Path | Plugin | Accessor | Default TTL | Max TTL | Token Type | Replication | Seal Wrap | External Entropy Access | Options | Description | UUID | Version"} for _, path := range paths { mount := auths[path] @@ -162,7 +162,7 @@ func (c *AuthListCommand) detailedMounts(auths map[string]*api.AuthMount) []stri pluginName = mount.Config.PluginName } - out = append(out, fmt.Sprintf("%s | %s | %s | %s | %s | %s | %s | %t | %v | %s | %s | %s", + out = append(out, fmt.Sprintf("%s | %s | %s | %s | %s | %s | %s | %t | %v | %s | %s | %s | %s", path, pluginName, mount.Accessor, @@ -175,6 +175,7 @@ func (c *AuthListCommand) detailedMounts(auths map[string]*api.AuthMount) []stri mount.Options, mount.Description, mount.UUID, + mount.Version, )) } diff --git a/command/plugin_deregister.go b/command/plugin_deregister.go index 7f0c4a614b8b4..1917bf9fd9c62 100644 --- a/command/plugin_deregister.go +++ b/command/plugin_deregister.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + semver "github.com/hashicorp/go-version" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/mitchellh/cli" @@ -17,6 +18,8 @@ var ( type PluginDeregisterCommand struct { *BaseCommand + + flagVersion string } func (c *PluginDeregisterCommand) Synopsis() string { @@ -28,20 +31,36 @@ func (c *PluginDeregisterCommand) Help() string { Usage: vault plugin deregister [options] TYPE NAME Deregister an existing plugin in the catalog. If the plugin does not exist, - no action is taken (the command is idempotent). The argument of type + no action is taken (the command is idempotent). The TYPE argument takes "auth", "database", or "secret". - Deregister the plugin named my-custom-plugin: + Deregister the unversioned auth plugin named my-custom-plugin: $ vault plugin deregister auth my-custom-plugin + Deregister the auth plugin named my-custom-plugin, version 1.0.0: + + $ vault plugin deregister -version=v1.0.0 auth my-custom-plugin + ` + c.Flags().Help() return strings.TrimSpace(helpText) } func (c *PluginDeregisterCommand) Flags() *FlagSets { - return c.flagSet(FlagSetHTTP) + set := c.flagSet(FlagSetHTTP) + + f := set.NewFlagSet("Command Options") + + f.StringVar(&StringVar{ + Name: "version", + Target: &c.flagVersion, + Completion: complete.PredictAnything, + Usage: "Version of the plugin to deregister. If unset, " + + "only an unversioned plugin may be deregistered.", + }) + + return set } func (c *PluginDeregisterCommand) AutocompleteArgs() complete.Predictor { @@ -62,21 +81,19 @@ func (c *PluginDeregisterCommand) Run(args []string) int { var pluginNameRaw, pluginTypeRaw string args = f.Args() - switch { - case len(args) < 1: - c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1 or 2, got %d)", len(args))) - return 1 - case len(args) > 2: - c.UI.Error(fmt.Sprintf("Too many arguments (expected 1 or 2, got %d)", len(args))) + switch len(args) { + case 0: + c.UI.Error("Not enough arguments (expected 1, or 2, got 0)") return 1 - - // These cases should come after invalid cases have been checked - case len(args) == 1: + case 1: pluginTypeRaw = "unknown" pluginNameRaw = args[0] - case len(args) == 2: + case 2: pluginTypeRaw = args[0] pluginNameRaw = args[1] + default: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, or 2, got %d)", len(args))) + return 1 } client, err := c.Client() @@ -91,10 +108,18 @@ func (c *PluginDeregisterCommand) Run(args []string) int { return 2 } pluginName := strings.TrimSpace(pluginNameRaw) + if c.flagVersion != "" { + _, err := semver.NewSemver(c.flagVersion) + if err != nil { + c.UI.Error(fmt.Sprintf("version %q is not a valid semantic version: %v", c.flagVersion, err)) + return 2 + } + } if err := client.Sys().DeregisterPlugin(&api.DeregisterPluginInput{ - Name: pluginName, - Type: pluginType, + Name: pluginName, + Type: pluginType, + Version: c.flagVersion, }); err != nil { c.UI.Error(fmt.Sprintf("Error deregistering plugin named %s: %s", pluginName, err)) return 2 diff --git a/command/plugin_deregister_test.go b/command/plugin_deregister_test.go index 9696c2f33c66d..7a6bc12d41bc8 100644 --- a/command/plugin_deregister_test.go +++ b/command/plugin_deregister_test.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/sdk/helper/consts" + "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) @@ -76,7 +77,7 @@ func TestPluginDeregisterCommand_Run(t *testing.T) { t.Run("integration", func(t *testing.T) { t.Parallel() - pluginDir, cleanup := testPluginDir(t) + pluginDir, cleanup := vault.MakeTestPluginDir(t) defer cleanup(t) client, _, closer := testVaultServerPluginDir(t, pluginDir) @@ -131,6 +132,101 @@ func TestPluginDeregisterCommand_Run(t *testing.T) { } }) + t.Run("integration with version", func(t *testing.T) { + t.Parallel() + + pluginDir, cleanup := vault.MakeTestPluginDir(t) + defer cleanup(t) + + client, _, closer := testVaultServerPluginDir(t, pluginDir) + defer closer() + + pluginName := "my-plugin" + _, _, version := testPluginCreateAndRegisterVersioned(t, client, pluginDir, pluginName, consts.PluginTypeCredential) + + ui, cmd := testPluginDeregisterCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-version=" + version, + consts.PluginTypeCredential.String(), + pluginName, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Deregistered plugin (if it was registered): " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + resp, err := client.Sys().ListPlugins(&api.ListPluginsInput{ + Type: consts.PluginTypeUnknown, + }) + if err != nil { + t.Fatal(err) + } + + found := false + for _, p := range resp.Details { + if p.Name == pluginName { + found = true + } + } + if found { + t.Errorf("expected %q to not be in %#v", pluginName, resp.Details) + } + }) + + t.Run("integration with missing version", func(t *testing.T) { + t.Parallel() + + pluginDir, cleanup := vault.MakeTestPluginDir(t) + defer cleanup(t) + + client, _, closer := testVaultServerPluginDir(t, pluginDir) + defer closer() + + pluginName := "my-plugin" + testPluginCreateAndRegisterVersioned(t, client, pluginDir, pluginName, consts.PluginTypeCredential) + + ui, cmd := testPluginDeregisterCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + consts.PluginTypeCredential.String(), + pluginName, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Deregistered plugin (if it was registered): " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + resp, err := client.Sys().ListPlugins(&api.ListPluginsInput{ + Type: consts.PluginTypeUnknown, + }) + if err != nil { + t.Fatal(err) + } + + found := false + for _, p := range resp.Details { + if p.Name == pluginName { + found = true + } + } + if !found { + t.Errorf("expected %q to be in %#v", pluginName, resp.Details) + } + }) + t.Run("communication_failure", func(t *testing.T) { t.Parallel() diff --git a/command/plugin_info_test.go b/command/plugin_info_test.go index 46dac68138c71..cfdab72ab3712 100644 --- a/command/plugin_info_test.go +++ b/command/plugin_info_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/hashicorp/vault/sdk/helper/consts" + "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) @@ -73,7 +74,7 @@ func TestPluginInfoCommand_Run(t *testing.T) { t.Run("default", func(t *testing.T) { t.Parallel() - pluginDir, cleanup := testPluginDir(t) + pluginDir, cleanup := vault.MakeTestPluginDir(t) defer cleanup(t) client, _, closer := testVaultServerPluginDir(t, pluginDir) @@ -104,7 +105,7 @@ func TestPluginInfoCommand_Run(t *testing.T) { t.Run("field", func(t *testing.T) { t.Parallel() - pluginDir, cleanup := testPluginDir(t) + pluginDir, cleanup := vault.MakeTestPluginDir(t) defer cleanup(t) client, _, closer := testVaultServerPluginDir(t, pluginDir) diff --git a/command/plugin_register.go b/command/plugin_register.go index 4a1eb19a5baba..60c9a7680d2b0 100644 --- a/command/plugin_register.go +++ b/command/plugin_register.go @@ -21,6 +21,7 @@ type PluginRegisterCommand struct { flagArgs []string flagCommand string flagSHA256 string + flagVersion string } func (c *PluginRegisterCommand) Synopsis() string { @@ -37,12 +38,13 @@ Usage: vault plugin register [options] TYPE NAME Register the plugin named my-custom-plugin: - $ vault plugin register -sha256=d3f0a8b... auth my-custom-plugin + $ vault plugin register -sha256=d3f0a8b... -version=v1.0.0 auth my-custom-plugin Register a plugin with custom arguments: $ vault plugin register \ -sha256=d3f0a8b... \ + -version=v1.0.0 \ -args=--with-glibc,--with-cgo \ auth my-custom-plugin @@ -79,6 +81,13 @@ func (c *PluginRegisterCommand) Flags() *FlagSets { Usage: "SHA256 of the plugin binary. This is required for all plugins.", }) + f.StringVar(&StringVar{ + Name: "version", + Target: &c.flagVersion, + Completion: complete.PredictAnything, + Usage: "Version of the plugin. Optional.", + }) + return set } @@ -144,6 +153,7 @@ func (c *PluginRegisterCommand) Run(args []string) int { Args: c.flagArgs, Command: command, SHA256: c.flagSHA256, + Version: c.flagVersion, }); err != nil { c.UI.Error(fmt.Sprintf("Error registering plugin %s: %s", pluginName, err)) return 2 diff --git a/command/plugin_register_test.go b/command/plugin_register_test.go index 05b358e6f4782..69031c46911f6 100644 --- a/command/plugin_register_test.go +++ b/command/plugin_register_test.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/sdk/helper/consts" + "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) @@ -77,7 +78,7 @@ func TestPluginRegisterCommand_Run(t *testing.T) { t.Run("integration", func(t *testing.T) { t.Parallel() - pluginDir, cleanup := testPluginDir(t) + pluginDir, cleanup := vault.MakeTestPluginDir(t) defer cleanup(t) client, _, closer := testVaultServerPluginDir(t, pluginDir) diff --git a/command/plugin_reload_test.go b/command/plugin_reload_test.go index 99b0c03c7f6e0..6c4982295b821 100644 --- a/command/plugin_reload_test.go +++ b/command/plugin_reload_test.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/sdk/helper/consts" + "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) @@ -82,7 +83,7 @@ func TestPluginReloadCommand_Run(t *testing.T) { t.Run("integration", func(t *testing.T) { t.Parallel() - pluginDir, cleanup := testPluginDir(t) + pluginDir, cleanup := vault.MakeTestPluginDir(t) defer cleanup(t) client, _, closer := testVaultServerPluginDir(t, pluginDir) diff --git a/command/plugin_test.go b/command/plugin_test.go index 786abdb52f4e9..be40abef8e142 100644 --- a/command/plugin_test.go +++ b/command/plugin_test.go @@ -6,36 +6,12 @@ import ( "io" "io/ioutil" "os" - "path/filepath" "testing" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/sdk/helper/consts" ) -// testPluginDir creates a temporary directory suitable for holding plugins. -// This helper also resolves symlinks to make tests happy on OS X. -func testPluginDir(tb testing.TB) (string, func(tb testing.TB)) { - tb.Helper() - - dir, err := ioutil.TempDir("", "") - if err != nil { - tb.Fatal(err) - } - - // OSX tempdir are /var, but actually symlinked to /private/var - dir, err = filepath.EvalSymlinks(dir) - if err != nil { - tb.Fatal(err) - } - - return dir, func(tb testing.TB) { - if err := os.RemoveAll(dir); err != nil { - tb.Fatal(err) - } - } -} - // testPluginCreate creates a sample plugin in a tempdir and returns the shasum // and filepath to the plugin. func testPluginCreate(tb testing.TB, dir, name string) (string, string) { @@ -78,3 +54,22 @@ func testPluginCreateAndRegister(tb testing.TB, client *api.Client, dir, name st return pth, sha256Sum } + +// testPluginCreateAndRegisterVersioned creates a versioned plugin and registers it in the catalog. +func testPluginCreateAndRegisterVersioned(tb testing.TB, client *api.Client, dir, name string, pluginType consts.PluginType) (string, string, string) { + tb.Helper() + + pth, sha256Sum := testPluginCreate(tb, dir, name) + + if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{ + Name: name, + Type: pluginType, + Command: name, + SHA256: sha256Sum, + Version: "v1.0.0", + }); err != nil { + tb.Fatal(err) + } + + return pth, sha256Sum, "v1.0.0" +} diff --git a/http/handler_test.go b/http/handler_test.go index 382c57c25056d..117f77959cff6 100644 --- a/http/handler_test.go +++ b/http/handler_test.go @@ -410,9 +410,13 @@ func TestSysMounts_headerAuth(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -424,9 +428,13 @@ func TestSysMounts_headerAuth(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), + "local": false, + "seal_wrap": true, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -437,9 +445,13 @@ func TestSysMounts_headerAuth(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), + "local": true, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -451,9 +463,13 @@ func TestSysMounts_headerAuth(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, }, "secret/": map[string]interface{}{ @@ -465,9 +481,13 @@ func TestSysMounts_headerAuth(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -479,9 +499,13 @@ func TestSysMounts_headerAuth(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), + "local": false, + "seal_wrap": true, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -492,9 +516,13 @@ func TestSysMounts_headerAuth(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), + "local": true, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -506,9 +534,13 @@ func TestSysMounts_headerAuth(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, } testResponseStatus(t, resp, 200) diff --git a/http/sys_auth_test.go b/http/sys_auth_test.go index e180959173c07..40f41dc786ddf 100644 --- a/http/sys_auth_test.go +++ b/http/sys_auth_test.go @@ -38,9 +38,13 @@ func TestSysAuth(t *testing.T) { "token_type": "default-service", "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, }, "token/": map[string]interface{}{ @@ -53,9 +57,13 @@ func TestSysAuth(t *testing.T) { "token_type": "default-service", "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, } testResponseStatus(t, resp, 200) @@ -114,9 +122,13 @@ func TestSysEnableAuth(t *testing.T) { "token_type": "default-service", "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "token/": map[string]interface{}{ "description": "token based credentials", @@ -128,9 +140,13 @@ func TestSysEnableAuth(t *testing.T) { "force_no_cache": false, "token_type": "default-service", }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, }, "foo/": map[string]interface{}{ @@ -143,9 +159,13 @@ func TestSysEnableAuth(t *testing.T) { "token_type": "default-service", "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "token/": map[string]interface{}{ "description": "token based credentials", @@ -157,9 +177,13 @@ func TestSysEnableAuth(t *testing.T) { "token_type": "default-service", "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, } testResponseStatus(t, resp, 200) @@ -224,6 +248,10 @@ func TestSysDisableAuth(t *testing.T) { "local": false, "seal_wrap": false, "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, }, "token/": map[string]interface{}{ @@ -239,6 +267,10 @@ func TestSysDisableAuth(t *testing.T) { "local": false, "seal_wrap": false, "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, } testResponseStatus(t, resp, 200) @@ -492,9 +524,13 @@ func TestSysRemountAuth(t *testing.T) { "token_type": "default-service", "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "token/": map[string]interface{}{ "description": "token based credentials", @@ -506,9 +542,13 @@ func TestSysRemountAuth(t *testing.T) { "force_no_cache": false, "token_type": "default-service", }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, }, "bar/": map[string]interface{}{ @@ -521,9 +561,13 @@ func TestSysRemountAuth(t *testing.T) { "token_type": "default-service", "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "token/": map[string]interface{}{ "description": "token based credentials", @@ -535,9 +579,13 @@ func TestSysRemountAuth(t *testing.T) { "token_type": "default-service", "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, } testResponseStatus(t, resp, 200) diff --git a/http/sys_mount_test.go b/http/sys_mount_test.go index 71c454a9e73e5..dd3d22007c324 100644 --- a/http/sys_mount_test.go +++ b/http/sys_mount_test.go @@ -39,9 +39,13 @@ func TestSysMounts(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -53,9 +57,13 @@ func TestSysMounts(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), + "local": false, + "seal_wrap": true, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -66,9 +74,13 @@ func TestSysMounts(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), + "local": true, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -80,9 +92,13 @@ func TestSysMounts(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, }, "secret/": map[string]interface{}{ @@ -94,9 +110,13 @@ func TestSysMounts(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -108,9 +128,13 @@ func TestSysMounts(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), + "local": false, + "seal_wrap": true, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -121,9 +145,13 @@ func TestSysMounts(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), + "local": true, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -135,9 +163,13 @@ func TestSysMounts(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, } testResponseStatus(t, resp, 200) @@ -196,9 +228,13 @@ func TestSysMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "secret/": map[string]interface{}{ "description": "key/value secret storage", @@ -209,9 +245,13 @@ func TestSysMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -223,9 +263,13 @@ func TestSysMount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), + "local": false, + "seal_wrap": true, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -236,9 +280,13 @@ func TestSysMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), + "local": true, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -250,9 +298,13 @@ func TestSysMount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, }, "foo/": map[string]interface{}{ @@ -264,9 +316,13 @@ func TestSysMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "secret/": map[string]interface{}{ "description": "key/value secret storage", @@ -277,9 +333,13 @@ func TestSysMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -291,9 +351,13 @@ func TestSysMount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), + "local": false, + "seal_wrap": true, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -304,9 +368,13 @@ func TestSysMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), + "local": true, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -318,9 +386,13 @@ func TestSysMount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, } testResponseStatus(t, resp, 200) @@ -414,9 +486,13 @@ func TestSysRemount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "secret/": map[string]interface{}{ "description": "key/value secret storage", @@ -427,9 +503,13 @@ func TestSysRemount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -441,9 +521,13 @@ func TestSysRemount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), + "local": false, + "seal_wrap": true, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -454,9 +538,13 @@ func TestSysRemount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), + "local": true, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -468,9 +556,13 @@ func TestSysRemount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, }, "bar/": map[string]interface{}{ @@ -482,9 +574,13 @@ func TestSysRemount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "secret/": map[string]interface{}{ "description": "key/value secret storage", @@ -495,9 +591,13 @@ func TestSysRemount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -509,9 +609,13 @@ func TestSysRemount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), + "local": false, + "seal_wrap": true, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -522,9 +626,13 @@ func TestSysRemount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), + "local": true, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -536,9 +644,13 @@ func TestSysRemount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, } testResponseStatus(t, resp, 200) @@ -597,9 +709,13 @@ func TestSysUnmount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -611,9 +727,13 @@ func TestSysUnmount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), + "local": false, + "seal_wrap": true, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -624,9 +744,13 @@ func TestSysUnmount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), + "local": true, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -638,9 +762,13 @@ func TestSysUnmount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, }, "secret/": map[string]interface{}{ @@ -652,9 +780,13 @@ func TestSysUnmount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -666,9 +798,13 @@ func TestSysUnmount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), + "local": false, + "seal_wrap": true, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -679,9 +815,13 @@ func TestSysUnmount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), + "local": true, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -693,9 +833,13 @@ func TestSysUnmount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, } testResponseStatus(t, resp, 200) @@ -840,9 +984,13 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "secret/": map[string]interface{}{ "description": "key/value secret storage", @@ -853,9 +1001,13 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -867,9 +1019,13 @@ func TestSysTuneMount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), + "local": false, + "seal_wrap": true, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -880,9 +1036,13 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), + "local": true, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -894,9 +1054,13 @@ func TestSysTuneMount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, }, "foo/": map[string]interface{}{ @@ -908,9 +1072,13 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "secret/": map[string]interface{}{ "description": "key/value secret storage", @@ -921,9 +1089,13 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -935,9 +1107,13 @@ func TestSysTuneMount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), + "local": false, + "seal_wrap": true, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -948,9 +1124,13 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), + "local": true, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -962,9 +1142,13 @@ func TestSysTuneMount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, } testResponseStatus(t, resp, 200) @@ -1049,9 +1233,13 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": json.Number("259200000"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "secret/": map[string]interface{}{ "description": "key/value secret storage", @@ -1062,9 +1250,13 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -1076,9 +1268,13 @@ func TestSysTuneMount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), + "local": false, + "seal_wrap": true, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -1089,9 +1285,13 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), + "local": true, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -1103,9 +1303,13 @@ func TestSysTuneMount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, }, "foo/": map[string]interface{}{ @@ -1117,9 +1321,13 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": json.Number("259200000"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "secret/": map[string]interface{}{ "description": "key/value secret storage", @@ -1130,9 +1338,13 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": false, - "seal_wrap": false, - "options": map[string]interface{}{"version": "1"}, + "local": false, + "seal_wrap": false, + "options": map[string]interface{}{"version": "1"}, + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -1144,9 +1356,13 @@ func TestSysTuneMount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": interface{}(nil), + "local": false, + "seal_wrap": true, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -1157,9 +1373,13 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": json.Number("0"), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": interface{}(nil), + "local": true, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -1171,9 +1391,13 @@ func TestSysTuneMount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []interface{}{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": interface{}(nil), + "local": false, + "seal_wrap": false, + "options": interface{}(nil), + "sha": "", + "running_sha": "", + "running_version": "", + "version": "", }, } diff --git a/sdk/helper/pluginutil/runner.go b/sdk/helper/pluginutil/runner.go index 370da22d1233e..2fb4849787987 100644 --- a/sdk/helper/pluginutil/runner.go +++ b/sdk/helper/pluginutil/runner.go @@ -5,7 +5,7 @@ import ( "time" log "github.com/hashicorp/go-hclog" - plugin "github.com/hashicorp/go-plugin" + "github.com/hashicorp/go-plugin" "github.com/hashicorp/go-version" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/wrapping" @@ -15,7 +15,8 @@ import ( // Looker defines the plugin Lookup function that looks into the plugin catalog // for available plugins and returns a PluginRunner type Looker interface { - LookupPlugin(context.Context, string, consts.PluginType) (*PluginRunner, error) + LookupPlugin(ctx context.Context, pluginName string, pluginType consts.PluginType) (*PluginRunner, error) + LookupPluginVersion(ctx context.Context, pluginName string, pluginType consts.PluginType, version string) (*PluginRunner, error) } // RunnerUtil interface defines the functions needed by the runner to wrap the diff --git a/sdk/logical/system_view.go b/sdk/logical/system_view.go index 83b4a951e842e..5e896b89e529b 100644 --- a/sdk/logical/system_view.go +++ b/sdk/logical/system_view.go @@ -54,7 +54,11 @@ type SystemView interface { // LookupPlugin looks into the plugin catalog for a plugin with the given // name. Returns a PluginRunner or an error if a plugin can not be found. - LookupPlugin(context.Context, string, consts.PluginType) (*pluginutil.PluginRunner, error) + LookupPlugin(ctx context.Context, pluginName string, pluginType consts.PluginType) (*pluginutil.PluginRunner, error) + + // LookupPluginVersion looks into the plugin catalog for a plugin with the given + // name and version. Returns a PluginRunner or an error if a plugin can not be found. + LookupPluginVersion(ctx context.Context, pluginName string, pluginType consts.PluginType, version string) (*pluginutil.PluginRunner, error) // NewPluginClient returns a client for managing the lifecycle of plugin // processes @@ -168,6 +172,10 @@ func (d StaticSystemView) LookupPlugin(_ context.Context, _ string, _ consts.Plu return nil, errors.New("LookupPlugin is not implemented in StaticSystemView") } +func (d StaticSystemView) LookupPluginVersion(_ context.Context, _ string, _ consts.PluginType, _ string) (*pluginutil.PluginRunner, error) { + return nil, errors.New("LookupPluginVersion is not implemented in StaticSystemView") +} + func (d StaticSystemView) MlockEnabled() bool { return d.EnableMlock } diff --git a/sdk/plugin/grpc_system.go b/sdk/plugin/grpc_system.go index 81bc324bf0fa6..58647e4a60638 100644 --- a/sdk/plugin/grpc_system.go +++ b/sdk/plugin/grpc_system.go @@ -107,6 +107,10 @@ func (s *gRPCSystemViewClient) LookupPlugin(_ context.Context, _ string, _ const return nil, fmt.Errorf("cannot call LookupPlugin from a plugin backend") } +func (s *gRPCSystemViewClient) LookupPluginVersion(_ context.Context, _ string, _ consts.PluginType, _ string) (*pluginutil.PluginRunner, error) { + return nil, fmt.Errorf("cannot call LookupPluginVersion from a plugin backend") +} + func (s *gRPCSystemViewClient) MlockEnabled() bool { reply, err := s.client.MlockEnabled(context.Background(), &pb.Empty{}) if err != nil { diff --git a/sdk/plugin/plugin.go b/sdk/plugin/plugin.go index 58163f2b3dccb..fafa684a63f32 100644 --- a/sdk/plugin/plugin.go +++ b/sdk/plugin/plugin.go @@ -6,7 +6,7 @@ import ( "fmt" log "github.com/hashicorp/go-hclog" - plugin "github.com/hashicorp/go-plugin" + "github.com/hashicorp/go-plugin" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/pluginutil" "github.com/hashicorp/vault/sdk/logical" @@ -28,13 +28,13 @@ func (b *BackendPluginClient) Cleanup(ctx context.Context) { b.client.Kill() } -// NewBackend will return an instance of an RPC-based client implementation of the backend for +// NewBackendWithVersion will return an instance of an RPC-based client implementation of the backend for // external plugins, or a concrete implementation of the backend if it is a builtin backend. // The backend is returned as a logical.Backend interface. The isMetadataMode param determines whether // the plugin should run in metadata mode. -func NewBackend(ctx context.Context, pluginName string, pluginType consts.PluginType, sys pluginutil.LookRunnerUtil, conf *logical.BackendConfig, isMetadataMode bool) (logical.Backend, error) { +func NewBackendWithVersion(ctx context.Context, pluginName string, pluginType consts.PluginType, sys pluginutil.LookRunnerUtil, conf *logical.BackendConfig, isMetadataMode bool, version string) (logical.Backend, error) { // Look for plugin in the plugin catalog - pluginRunner, err := sys.LookupPlugin(ctx, pluginName, pluginType) + pluginRunner, err := sys.LookupPluginVersion(ctx, pluginName, pluginType, version) if err != nil { return nil, err } @@ -66,6 +66,14 @@ func NewBackend(ctx context.Context, pluginName string, pluginType consts.Plugin return backend, nil } +// NewBackend will return an instance of an RPC-based client implementation of the backend for +// external plugins, or a concrete implementation of the backend if it is a builtin backend. +// The backend is returned as a logical.Backend interface. The isMetadataMode param determines whether +// the plugin should run in metadata mode. +func NewBackend(ctx context.Context, pluginName string, pluginType consts.PluginType, sys pluginutil.LookRunnerUtil, conf *logical.BackendConfig, isMetadataMode bool) (logical.Backend, error) { + return NewBackendWithVersion(ctx, pluginName, pluginType, sys, conf, isMetadataMode, "") +} + 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. pluginSet := map[int]plugin.PluginSet{ diff --git a/vault/auth.go b/vault/auth.go index 0720cc656b50c..1211413710f57 100644 --- a/vault/auth.go +++ b/vault/auth.go @@ -911,7 +911,7 @@ func (c *Core) newCredentialBackend(ctx context.Context, entry *MountEntry, sysV f, ok := c.credentialBackends[t] if !ok { - plug, err := c.pluginCatalog.Get(ctx, t, consts.PluginTypeCredential, "") + plug, err := c.pluginCatalog.Get(ctx, t, consts.PluginTypeCredential, entry.Version) if err != nil { return nil, err } @@ -938,6 +938,7 @@ func (c *Core) newCredentialBackend(ctx context.Context, entry *MountEntry, sysV } conf["plugin_type"] = consts.PluginTypeCredential.String() + conf["plugin_version"] = entry.Version authLogger := c.baseLogger.Named(fmt.Sprintf("auth.%s.%s", t, entry.Accessor)) c.AddLogger(authLogger) diff --git a/vault/auth_external_plugin_test.go b/vault/auth_external_plugin_test.go new file mode 100644 index 0000000000000..515f4df05e5d9 --- /dev/null +++ b/vault/auth_external_plugin_test.go @@ -0,0 +1,318 @@ +package vault + +import ( + "context" + "crypto/sha256" + "errors" + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "sync" + "testing" + + "github.com/hashicorp/vault/helper/namespace" + "github.com/hashicorp/vault/sdk/framework" +) + +var ( + compileOnce sync.Once + pluginBytes []byte +) + +func testCoreWithPlugin(t *testing.T) (*Core, string, string) { + t.Helper() + pluginName, pluginSha256, pluginDir := compilePlugin(t) + conf := &CoreConfig{ + BuiltinRegistry: NewMockBuiltinRegistry(), + PluginDirectory: pluginDir, + } + core := TestCoreWithSealAndUI(t, conf) + core, _, _ = testCoreUnsealed(t, core) + return core, pluginName, pluginSha256 +} + +// to mount a plugin, we need a working binary plugin, so we compile one here. +func compilePlugin(t *testing.T) (string, string, string) { + pluginType := "approle" + pluginName := "vault-plugin-auth-" + pluginType + + dir := "" + // detect if we are in the "vault/" or the root directory and compensate + if _, err := os.Stat("builtin"); os.IsNotExist(err) { + wd, err := os.Getwd() + if err != nil { + panic(err) + } + dir = filepath.Dir(wd) + } + + pluginDir, cleanup := MakeTestPluginDir(t) + t.Cleanup(func() { cleanup(t) }) + + pluginPath := path.Join(pluginDir, pluginName) + + // cache the compilation to only run once + compileOnce.Do(func() { + cmd := exec.Command("go", "build", "-o", pluginPath, fmt.Sprintf("builtin/credential/%s/cmd/%s/main.go", pluginType, pluginType)) + cmd.Dir = dir + output, err := cmd.CombinedOutput() + if err != nil { + panic(fmt.Errorf("error running go build %v output: %s", err, output)) + } + pluginBytes, err = os.ReadFile(pluginPath) + if err != nil { + panic(err) + } + }) + + // write the cached plugin if necessary + var err error + if _, err := os.Stat(pluginPath); os.IsNotExist(err) { + err = os.WriteFile(pluginPath, pluginBytes, 0o777) + } + if err != nil { + t.Fatal(err) + } + + sha := sha256.New() + _, err = sha.Write(pluginBytes) + if err != nil { + t.Fatal(err) + } + return pluginName, fmt.Sprintf("%x", sha.Sum(nil)), pluginDir +} + +func TestCore_EnableExternalCredentialPlugin(t *testing.T) { + c, pluginName, pluginSha256 := testCoreWithPlugin(t) + d := &framework.FieldData{ + Raw: map[string]interface{}{ + "name": pluginName, + "sha256": pluginSha256, + "version": "v1.0.0", + "command": pluginName, + }, + Schema: c.systemBackend.pluginsCatalogCRUDPath().Fields, + } + resp, err := c.systemBackend.handlePluginCatalogUpdate(context.Background(), nil, d) + if err != nil { + t.Fatal(err) + } + if resp.Error() != nil { + t.Fatalf("%#v", resp) + } + me := &MountEntry{ + Table: credentialTableType, + Path: "foo", + Type: pluginName, + Version: "v1.0.0", + } + err = c.enableCredential(namespace.RootContext(nil), me) + if err != nil { + t.Fatalf("err: %v", err) + } + + match := c.router.MatchingMount(namespace.RootContext(nil), "auth/foo/bar") + if match != "auth/foo/" { + t.Fatalf("missing mount, match: %q", match) + } +} + +func TestCore_EnableExternalCredentialPlugin_MultipleVersions(t *testing.T) { + c, pluginName, pluginSha256 := testCoreWithPlugin(t) + d := &framework.FieldData{ + Raw: map[string]interface{}{ + "name": pluginName, + "sha256": pluginSha256, + "version": "v1.0.0", + "command": pluginName, + }, + Schema: c.systemBackend.pluginsCatalogCRUDPath().Fields, + } + resp, err := c.systemBackend.handlePluginCatalogUpdate(context.Background(), nil, d) + if err != nil { + t.Fatal(err) + } + if resp.Error() != nil { + t.Fatalf("%#v", resp) + } + + d = &framework.FieldData{ + Raw: map[string]interface{}{ + "name": pluginName, + "sha256": pluginSha256, + "version": "v1.0.1", + "command": pluginName, + }, + Schema: c.systemBackend.pluginsCatalogCRUDPath().Fields, + } + resp, err = c.systemBackend.handlePluginCatalogUpdate(context.Background(), nil, d) + if err != nil { + t.Fatal(err) + } + if resp.Error() != nil { + t.Fatalf("%#v", resp) + } + + me := &MountEntry{ + Table: credentialTableType, + Path: "foo", + Type: pluginName, + Version: "v1.0.0", + } + err = c.enableCredential(namespace.RootContext(nil), me) + if err != nil { + t.Fatalf("err: %v", err) + } + + match := c.router.MatchingMount(namespace.RootContext(nil), "auth/foo/bar") + if match != "auth/foo/" { + t.Fatalf("missing mount, match: %q", match) + } + + raw, _ := c.router.root.Get(match) + if raw.(*routeEntry).mountEntry.Version != "v1.0.0" { + t.Errorf("Expected mount to be version v1.0.0 but got %s", raw.(*routeEntry).mountEntry.Version) + } +} + +func TestCore_EnableExternalCredentialPlugin_MultipleVersions_MountSecond(t *testing.T) { + c, pluginName, pluginSha256 := testCoreWithPlugin(t) + d := &framework.FieldData{ + Raw: map[string]interface{}{ + "name": pluginName, + "sha256": pluginSha256, + "command": pluginName, + "version": "v1.0.0", + }, + Schema: c.systemBackend.pluginsCatalogCRUDPath().Fields, + } + resp, err := c.systemBackend.handlePluginCatalogUpdate(context.Background(), nil, d) + if err != nil { + t.Fatal(err) + } + if resp.Error() != nil { + t.Fatalf("%#v", resp) + } + + d = &framework.FieldData{ + Raw: map[string]interface{}{ + "name": pluginName, + "sha256": pluginSha256, + "version": "v1.0.1", + "command": pluginName, + }, + Schema: c.systemBackend.pluginsCatalogCRUDPath().Fields, + } + resp, err = c.systemBackend.handlePluginCatalogUpdate(context.Background(), nil, d) + if err != nil { + t.Fatal(err) + } + if resp.Error() != nil { + t.Fatalf("%#v", resp) + } + + me := &MountEntry{ + Table: credentialTableType, + Path: "foo", + Type: pluginName, + Version: "v1.0.1", + } + err = c.enableCredential(namespace.RootContext(nil), me) + if err != nil { + t.Fatalf("err: %v", err) + } + + match := c.router.MatchingMount(namespace.RootContext(nil), "auth/foo/bar") + if match != "auth/foo/" { + t.Fatalf("missing mount, match: %q", match) + } + + raw, _ := c.router.root.Get(match) + if raw.(*routeEntry).mountEntry.Version != "v1.0.1" { + t.Errorf("Expected mount to be version v1.0.1 but got %s", raw.(*routeEntry).mountEntry.Version) + } +} + +func TestCore_EnableExternalCredentialPlugin_NoVersionsOkay(t *testing.T) { + c, pluginName, pluginSha256 := testCoreWithPlugin(t) + d := &framework.FieldData{ + Raw: map[string]interface{}{ + "name": pluginName, + "sha256": pluginSha256, + "command": pluginName, + }, + Schema: c.systemBackend.pluginsCatalogCRUDPath().Fields, + } + resp, err := c.systemBackend.handlePluginCatalogUpdate(context.Background(), nil, d) + if err != nil { + t.Fatal(err) + } + if resp.Error() != nil { + t.Fatalf("%#v", resp) + } + + me := &MountEntry{ + Table: credentialTableType, + Path: "foo", + Type: pluginName, + } + err = c.enableCredential(namespace.RootContext(nil), me) + if err != nil { + t.Fatalf("err: %v", err) + } + + match := c.router.MatchingMount(namespace.RootContext(nil), "auth/foo/bar") + if match != "auth/foo/" { + t.Fatalf("missing mount, match: %q", match) + } +} + +func TestCore_EnableExternalCredentialPlugin_NoVersionOnRegister(t *testing.T) { + c, pluginName, pluginSha256 := testCoreWithPlugin(t) + d := &framework.FieldData{ + Raw: map[string]interface{}{ + "name": pluginName, + "sha256": pluginSha256, + "command": pluginName, + }, + Schema: c.systemBackend.pluginsCatalogCRUDPath().Fields, + } + resp, err := c.systemBackend.handlePluginCatalogUpdate(context.Background(), nil, d) + if err != nil { + t.Fatal(err) + } + if resp.Error() != nil { + t.Fatalf("%#v", resp) + } + + me := &MountEntry{ + Table: credentialTableType, + Path: "foo", + Type: pluginName, + Version: "v1.0.0", + } + err = c.enableCredential(namespace.RootContext(nil), me) + if err == nil || !errors.Is(err, ErrPluginNotFound) { + t.Fatalf("Expected to get plugin not found but got: %v", err) + } +} + +func TestCore_EnableExternalCredentialPlugin_InvalidName(t *testing.T) { + c, pluginName, pluginSha256 := testCoreWithPlugin(t) + d := &framework.FieldData{ + Raw: map[string]interface{}{ + "name": pluginName, + "sha256": pluginSha256, + "version": "v1.0.0", + "command": pluginName + "xyz", + }, + Schema: c.systemBackend.pluginsCatalogCRUDPath().Fields, + } + _, err := c.systemBackend.handlePluginCatalogUpdate(context.Background(), nil, d) + if err == nil || !strings.Contains(err.Error(), "no such file or directory") { + t.Fatalf("should have gotten a no such file or directory error inserting the plugin: %v", err) + } +} diff --git a/vault/auth_test.go b/vault/auth_test.go index 1ecf0495588f6..c2667308bb408 100644 --- a/vault/auth_test.go +++ b/vault/auth_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - metrics "github.com/armon/go-metrics" + "github.com/armon/go-metrics" "github.com/hashicorp/vault/helper/metricsutil" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/helper/jsonutil" diff --git a/vault/dynamic_system_view.go b/vault/dynamic_system_view.go index 124a134d96101..80663385e58d1 100644 --- a/vault/dynamic_system_view.go +++ b/vault/dynamic_system_view.go @@ -234,13 +234,19 @@ func (d dynamicSystemView) NewPluginClient(ctx context.Context, config pluginuti // LookupPlugin looks for a plugin with the given name in the plugin catalog. It // returns a PluginRunner or an error if no plugin was found. func (d dynamicSystemView) LookupPlugin(ctx context.Context, name string, pluginType consts.PluginType) (*pluginutil.PluginRunner, error) { + return d.LookupPluginVersion(ctx, name, pluginType, "") +} + +// LookupPluginVersion looks for a plugin with the given name and version in the plugin catalog. It +// returns a PluginRunner or an error if no plugin was found. +func (d dynamicSystemView) LookupPluginVersion(ctx context.Context, name string, pluginType consts.PluginType, version string) (*pluginutil.PluginRunner, error) { if d.core == nil { return nil, fmt.Errorf("system view core is nil") } if d.core.pluginCatalog == nil { return nil, fmt.Errorf("system view core plugin catalog is nil") } - r, err := d.core.pluginCatalog.Get(ctx, name, pluginType, "") + r, err := d.core.pluginCatalog.Get(ctx, name, pluginType, version) if err != nil { return nil, err } diff --git a/vault/logical_system.go b/vault/logical_system.go index e8d344b6fb9e3..7b768ea265410 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -620,7 +620,8 @@ func getVersion(d *framework.FieldData) (string, error) { } // Canonicalize the version string. - version = semanticVersion.String() + // Add the 'v' back in, since semantic version strips it out, and we want to be consistent with internal plugins. + version = "v" + semanticVersion.String() } return version, nil @@ -888,6 +889,10 @@ func mountInfo(entry *MountEntry) map[string]interface{} { "external_entropy_access": entry.ExternalEntropyAccess, "options": entry.Options, "uuid": entry.UUID, + "version": entry.Version, + "sha": entry.Sha, + "running_version": entry.RunningVersion, + "running_sha": entry.RunningSha, } entryConfig := map[string]interface{}{ "default_lease_ttl": int64(entry.Config.DefaultLeaseTTL.Seconds()), @@ -981,6 +986,14 @@ func (b *SystemBackend) handleMount(ctx context.Context, req *logical.Request, d sealWrap := data.Get("seal_wrap").(bool) externalEntropyAccess := data.Get("external_entropy_access").(bool) options := data.Get("options").(map[string]string) + version := data.Get("version").(string) + if version != "" { + v, err := semver.NewSemver(version) + if err != nil { + return nil, err + } + version = "v" + v.String() + } var config MountConfig var apiConfig APIMountConfig @@ -1123,6 +1136,7 @@ func (b *SystemBackend) handleMount(ctx context.Context, req *logical.Request, d SealWrap: sealWrap, ExternalEntropyAccess: externalEntropyAccess, Options: options, + Version: version, } // Attempt mount @@ -2223,6 +2237,14 @@ func (b *SystemBackend) handleEnableAuth(ctx context.Context, req *logical.Reque sealWrap := data.Get("seal_wrap").(bool) externalEntropyAccess := data.Get("external_entropy_access").(bool) options := data.Get("options").(map[string]string) + version := data.Get("version").(string) + if version != "" { + v, err := semver.NewSemver(version) + if err != nil { + return nil, err + } + version = "v" + v.String() + } var config MountConfig var apiConfig APIMountConfig @@ -2354,6 +2376,7 @@ func (b *SystemBackend) handleEnableAuth(ctx context.Context, req *logical.Reque SealWrap: sealWrap, ExternalEntropyAccess: externalEntropyAccess, Options: options, + Version: version, } // Attempt enabling diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index 11cf1b655d07e..ee666adc63410 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -1600,6 +1600,10 @@ func (b *SystemBackend) authPaths() []*framework.Path { Type: framework.TypeKVPairs, Description: strings.TrimSpace(sysHelp["auth_options"][0]), }, + "version": { + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["plugin-catalog_version"][0]), + }, }, Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{ @@ -1971,6 +1975,10 @@ func (b *SystemBackend) mountPaths() []*framework.Path { Type: framework.TypeKVPairs, Description: strings.TrimSpace(sysHelp["mount_options"][0]), }, + "version": { + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["plugin-catalog_version"][0]), + }, }, Operations: map[logical.Operation]framework.OperationHandler{ diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 45aca583f7b85..17156b41e7476 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -17,7 +17,7 @@ import ( "github.com/fatih/structs" "github.com/go-test/deep" - hclog "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-hclog" semver "github.com/hashicorp/go-version" "github.com/hashicorp/vault/audit" credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" @@ -172,6 +172,10 @@ func TestSystemBackend_mounts(t *testing.T) { "options": map[string]string{ "version": "1", }, + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, "sys/": map[string]interface{}{ "type": "system", @@ -185,9 +189,13 @@ func TestSystemBackend_mounts(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []string{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": map[string]string(nil), + "local": false, + "seal_wrap": true, + "options": map[string]string(nil), + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -200,9 +208,13 @@ func TestSystemBackend_mounts(t *testing.T) { "max_lease_ttl": resp.Data["cubbyhole/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": map[string]string(nil), + "local": true, + "seal_wrap": false, + "options": map[string]string(nil), + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -216,9 +228,13 @@ func TestSystemBackend_mounts(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []string{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": map[string]string(nil), + "local": false, + "seal_wrap": false, + "options": map[string]string(nil), + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, } if diff := deep.Equal(resp.Data, exp); len(diff) > 0 { @@ -285,6 +301,10 @@ func TestSystemBackend_mount(t *testing.T) { "options": map[string]string{ "version": "1", }, + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, "sys/": map[string]interface{}{ "type": "system", @@ -298,9 +318,13 @@ func TestSystemBackend_mount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []string{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": map[string]string(nil), + "local": false, + "seal_wrap": true, + "options": map[string]string(nil), + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -313,9 +337,13 @@ func TestSystemBackend_mount(t *testing.T) { "max_lease_ttl": resp.Data["cubbyhole/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": map[string]string(nil), + "local": true, + "seal_wrap": false, + "options": map[string]string(nil), + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -329,9 +357,13 @@ func TestSystemBackend_mount(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []string{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": map[string]string(nil), + "local": false, + "seal_wrap": false, + "options": map[string]string(nil), + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, "prod/secret/": map[string]interface{}{ "description": "", @@ -349,6 +381,10 @@ func TestSystemBackend_mount(t *testing.T) { "options": map[string]string{ "version": "1", }, + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, } if diff := deep.Equal(resp.Data, exp); len(diff) > 0 { @@ -1818,9 +1854,13 @@ func TestSystemBackend_authTable(t *testing.T) { "force_no_cache": false, "token_type": "default-service", }, - "local": false, - "seal_wrap": false, - "options": map[string]string(nil), + "local": false, + "seal_wrap": false, + "options": map[string]string(nil), + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, } if diff := deep.Equal(resp.Data, exp); diff != nil { @@ -1882,9 +1922,13 @@ func TestSystemBackend_enableAuth(t *testing.T) { "force_no_cache": false, "token_type": "default-service", }, - "local": true, - "seal_wrap": true, - "options": map[string]string{}, + "local": true, + "seal_wrap": true, + "options": map[string]string{}, + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, "token/": map[string]interface{}{ "type": "token", @@ -1898,9 +1942,13 @@ func TestSystemBackend_enableAuth(t *testing.T) { "force_no_cache": false, "token_type": "default-service", }, - "local": false, - "seal_wrap": false, - "options": map[string]string(nil), + "local": false, + "seal_wrap": false, + "options": map[string]string(nil), + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, } if diff := deep.Equal(resp.Data, exp); diff != nil { @@ -2893,21 +2941,25 @@ func TestSystemBackend_rotate(t *testing.T) { } func testSystemBackend(t *testing.T) logical.Backend { + t.Helper() c, _, _ := TestCoreUnsealed(t) return c.systemBackend } func testSystemBackendRaw(t *testing.T) logical.Backend { + t.Helper() c, _, _ := TestCoreUnsealedRaw(t) return c.systemBackend } func testCoreSystemBackend(t *testing.T) (*Core, logical.Backend, string) { + t.Helper() c, _, root := TestCoreUnsealed(t) return c, c.systemBackend, root } func testCoreSystemBackendRaw(t *testing.T) (*Core, logical.Backend, string) { + t.Helper() c, _, root := TestCoreUnsealedRaw(t) return c, c.systemBackend, root } @@ -3033,7 +3085,7 @@ func TestSystemBackend_PluginCatalog_CRUD(t *testing.T) { "args": []string{"--test"}, "sha256": "31", "builtin": false, - "version": "0.1.0", + "version": "v0.1.0", } if !reflect.DeepEqual(actual, expected) { t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", actual, expected) @@ -3270,6 +3322,10 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { "options": map[string]string{ "version": "1", }, + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, "sys/": map[string]interface{}{ "type": "system", @@ -3283,9 +3339,13 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []string{"Accept"}, }, - "local": false, - "seal_wrap": true, - "options": map[string]string(nil), + "local": false, + "seal_wrap": true, + "options": map[string]string(nil), + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -3298,9 +3358,13 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { "max_lease_ttl": resp.Data["secret"].(map[string]interface{})["cubbyhole/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), "force_no_cache": false, }, - "local": true, - "seal_wrap": false, - "options": map[string]string(nil), + "local": true, + "seal_wrap": false, + "options": map[string]string(nil), + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, "identity/": map[string]interface{}{ "description": "identity store", @@ -3314,9 +3378,13 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { "force_no_cache": false, "passthrough_request_headers": []string{"Authorization"}, }, - "local": false, - "seal_wrap": false, - "options": map[string]string(nil), + "local": false, + "seal_wrap": false, + "options": map[string]string(nil), + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, }, "auth": map[string]interface{}{ @@ -3335,6 +3403,10 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { "uuid": resp.Data["auth"].(map[string]interface{})["token/"].(map[string]interface{})["uuid"], "local": false, "seal_wrap": false, + "sha": "", + "running_sha": "", + "version": "", + "running_version": "", }, }, } diff --git a/vault/mount.go b/vault/mount.go index f69cd072c55be..7906ddfad96d2 100644 --- a/vault/mount.go +++ b/vault/mount.go @@ -12,7 +12,7 @@ import ( "github.com/armon/go-metrics" "github.com/hashicorp/go-secure-stdlib/strutil" - uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/builtin/plugin" "github.com/hashicorp/vault/helper/metricsutil" "github.com/hashicorp/vault/helper/namespace" @@ -328,6 +328,12 @@ type MountEntry struct { // without separately managing their locks individually. See SyncCache() for // the specific values that are being cached. synthesizedConfigCache sync.Map + + // version info + Version string `json:"version,omitempty"` + Sha string `json:"sha,omitempty"` + RunningVersion string `json:"running_version,omitempty"` + RunningSha string `json:"running_sha,omitempty"` } // MountConfig is used to hold settable options @@ -1419,7 +1425,7 @@ func (c *Core) newLogicalBackend(ctx context.Context, entry *MountEntry, sysView f, ok := c.logicalBackends[t] if !ok { - plug, err := c.pluginCatalog.Get(ctx, t, consts.PluginTypeSecrets, "") + plug, err := c.pluginCatalog.Get(ctx, t, consts.PluginTypeSecrets, entry.Version) if err != nil { return nil, err } diff --git a/vault/testing.go b/vault/testing.go index 9a9b5f5e5a79d..c07b3b4616ead 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -2424,3 +2424,37 @@ func CreateTestClusterWithRollbackPeriod(t testing.T, newPeriod time.Duration, b // Return the cluster. return cluster } + +// MakeTestPluginDir creates a temporary directory suitable for holding plugins. +// This helper also resolves symlinks to make tests happy on OS X. +func MakeTestPluginDir(t testing.T) (string, func(t testing.T)) { + if t != nil { + t.Helper() + } + + dir, err := os.MkdirTemp("", "") + if err != nil { + if t == nil { + panic(err) + } + t.Fatal(err) + } + + // OSX tempdir are /var, but actually symlinked to /private/var + dir, err = filepath.EvalSymlinks(dir) + if err != nil { + if t == nil { + panic(err) + } + t.Fatal(err) + } + + return dir, func(t testing.T) { + if err := os.RemoveAll(dir); err != nil { + if t == nil { + panic(err) + } + t.Fatal(err) + } + } +}