diff --git a/Makefile b/Makefile index 3a5d9a4d7..f084e3bd5 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ EXTERNAL_TOOLS_CI=\ EXTERNAL_TOOLS=\ github.com/client9/misspell/cmd/misspell GOFMT_FILES?=$$(find . -name '*.go' | grep -v pb.go | grep -v vendor) +SED?=$(shell command -v gsed || command -v sed) GO_VERSION_MIN=1.19.1 @@ -190,8 +191,8 @@ proto: bootstrap # No additional sed expressions should be added to this list. Going forward # we should just use the variable names choosen by protobuf. These are left # here for backwards compatability, namely for SDK compilation. - sed -i -e 's/Id/ID/' vault/request_forwarding_service.pb.go - sed -i -e 's/Idp/IDP/' -e 's/Url/URL/' -e 's/Id/ID/' -e 's/IDentity/Identity/' -e 's/EntityId/EntityID/' -e 's/Api/API/' -e 's/Qr/QR/' -e 's/Totp/TOTP/' -e 's/Mfa/MFA/' -e 's/Pingid/PingID/' -e 's/namespaceId/namespaceID/' -e 's/Ttl/TTL/' -e 's/BoundCidrs/BoundCIDRs/' helper/identity/types.pb.go helper/identity/mfa/types.pb.go helper/storagepacker/types.pb.go sdk/plugin/pb/backend.pb.go sdk/logical/identity.pb.go vault/activity/activity_log.pb.go + $(SED) -i -e 's/Id/ID/' vault/request_forwarding_service.pb.go + $(SED) -i -e 's/Idp/IDP/' -e 's/Url/URL/' -e 's/Id/ID/' -e 's/IDentity/Identity/' -e 's/EntityId/EntityID/' -e 's/Api/API/' -e 's/Qr/QR/' -e 's/Totp/TOTP/' -e 's/Mfa/MFA/' -e 's/Pingid/PingID/' -e 's/namespaceId/namespaceID/' -e 's/Ttl/TTL/' -e 's/BoundCidrs/BoundCIDRs/' helper/identity/types.pb.go helper/identity/mfa/types.pb.go helper/storagepacker/types.pb.go sdk/plugin/pb/backend.pb.go sdk/logical/identity.pb.go vault/activity/activity_log.pb.go # This will inject the sentinel struct tags as decorated in the proto files. protoc-go-inject-tag -input=./helper/identity/types.pb.go diff --git a/builtin/logical/database/version_wrapper.go b/builtin/logical/database/version_wrapper.go index 2e4506390..734c36ca6 100644 --- a/builtin/logical/database/version_wrapper.go +++ b/builtin/logical/database/version_wrapper.go @@ -9,6 +9,7 @@ import ( v4 "github.com/hashicorp/vault/sdk/database/dbplugin" v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "github.com/hashicorp/vault/sdk/helper/pluginutil" + "github.com/hashicorp/vault/sdk/logical" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -18,6 +19,8 @@ type databaseVersionWrapper struct { v5 v5.Database } +var _ logical.PluginVersioner = databaseVersionWrapper{} + // newDatabaseWrapper figures out which version of the database the pluginName is referring to and returns a wrapper object // that can be used to make operations on the underlying database plugin. func newDatabaseWrapper(ctx context.Context, pluginName string, pluginVersion string, sys pluginutil.LookRunnerUtil, logger log.Logger) (dbw databaseVersionWrapper, err error) { @@ -227,6 +230,21 @@ func (d databaseVersionWrapper) Close() error { return d.v4.Close() } +func (d databaseVersionWrapper) PluginVersion() logical.PluginVersion { + // v5 Database + if d.isV5() { + if versioner, ok := d.v5.(logical.PluginVersioner); ok { + return versioner.PluginVersion() + } + } + + // v4 Database + if versioner, ok := d.v4.(logical.PluginVersioner); ok { + return versioner.PluginVersion() + } + return logical.EmptyPluginVersion +} + func (d databaseVersionWrapper) isV5() bool { return d.v5 != nil } diff --git a/builtin/plugin/backend.go b/builtin/plugin/backend.go index f4b9c23d1..92b6b4327 100644 --- a/builtin/plugin/backend.go +++ b/builtin/plugin/backend.go @@ -10,7 +10,7 @@ import ( log "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-multierror" - uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/go-uuid" v5 "github.com/hashicorp/vault/builtin/plugin/v5" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/consts" @@ -79,15 +79,28 @@ func Backend(ctx context.Context, conf *logical.BackendConfig) (*PluginBackend, // Get SpecialPaths and BackendType paths := raw.SpecialPaths() btype := raw.Type() + runningVersion := "" + if versioner, ok := raw.(logical.PluginVersioner); ok { + runningVersion = versioner.PluginVersion().Version + } + + external := false + if externaler, ok := raw.(logical.Externaler); ok { + external = externaler.IsExternal() + } // Cleanup meta plugin backend raw.Cleanup(ctx) - // Initialize b.Backend with dummy backend since plugin + // Initialize b.Backend with placeholder backend since plugin // backends will need to be lazy loaded. - b.Backend = &framework.Backend{ - PathsSpecial: paths, - BackendType: btype, + b.Backend = &placeholderBackend{ + Backend: framework.Backend{ + PathsSpecial: paths, + BackendType: btype, + RunningVersion: runningVersion, + }, + external: external, } b.config = conf @@ -95,6 +108,23 @@ func Backend(ctx context.Context, conf *logical.BackendConfig) (*PluginBackend, return &b, nil } +// placeholderBackend is used a placeholder before a backend is lazy-loaded. +// It is mostly used to mark that the backend is an external backend. +type placeholderBackend struct { + framework.Backend + + external bool +} + +func (p *placeholderBackend) IsExternal() bool { + return p.external +} + +var ( + _ logical.Externaler = (*placeholderBackend)(nil) + _ logical.PluginVersioner = (*placeholderBackend)(nil) +) + // PluginBackend is a thin wrapper around plugin.BackendPluginClient type PluginBackend struct { Backend logical.Backend @@ -286,3 +316,22 @@ func (b *PluginBackend) Type() logical.BackendType { defer b.RUnlock() return b.Backend.Type() } + +func (b *PluginBackend) PluginVersion() logical.PluginVersion { + if versioner, ok := b.Backend.(logical.PluginVersioner); ok { + return versioner.PluginVersion() + } + return logical.EmptyPluginVersion +} + +func (b *PluginBackend) IsExternal() bool { + if externaler, ok := b.Backend.(logical.Externaler); ok { + return externaler.IsExternal() + } + return false +} + +var ( + _ logical.PluginVersioner = (*PluginBackend)(nil) + _ logical.Externaler = (*PluginBackend)(nil) +) diff --git a/builtin/plugin/v5/backend.go b/builtin/plugin/v5/backend.go index 553933515..96f6985d2 100644 --- a/builtin/plugin/v5/backend.go +++ b/builtin/plugin/v5/backend.go @@ -5,7 +5,7 @@ import ( "net/rpc" "sync" - uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/plugin" @@ -35,7 +35,7 @@ func Backend(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, return &b, nil } -// backend is a thin wrapper around plugin.BackendPluginClientV5 +// backend is a thin wrapper around a builtin plugin or a plugin.BackendPluginClientV5 type backend struct { logical.Backend mu sync.RWMutex @@ -147,3 +147,11 @@ func (b *backend) InvalidateKey(ctx context.Context, key string) { defer b.mu.RUnlock() b.Backend.InvalidateKey(ctx, key) } + +func (b *backend) IsExternal() bool { + switch b.Backend.(type) { + case *plugin.BackendPluginClientV5: + return true + } + return false +} diff --git a/changelog/17088.txt b/changelog/17088.txt new file mode 100644 index 000000000..dfd08c9a2 --- /dev/null +++ b/changelog/17088.txt @@ -0,0 +1,3 @@ +```release-note:improvement +plugins: Adding version to plugin GRPC interface +``` \ No newline at end of file diff --git a/helper/versions/version.go b/helper/versions/version.go new file mode 100644 index 000000000..1fa4e36e2 --- /dev/null +++ b/helper/versions/version.go @@ -0,0 +1,54 @@ +package versions + +import ( + "fmt" + "runtime/debug" + "strings" + "sync" + + "github.com/hashicorp/vault/sdk/helper/consts" + "github.com/hashicorp/vault/sdk/version" +) + +var ( + buildInfoOnce sync.Once // once is used to ensure we only parse build info once. + buildInfo *debug.BuildInfo + DefaultBuiltinVersion = "v" + version.GetVersion().Version + "+builtin.vault" +) + +func GetBuiltinVersion(pluginType consts.PluginType, pluginName string) string { + buildInfoOnce.Do(func() { + buildInfo, _ = debug.ReadBuildInfo() + }) + + // Should never happen, means the binary was built without Go modules. + // Fall back to just the Vault version. + if buildInfo == nil { + return DefaultBuiltinVersion + } + + // Vault builtin plugins are all either: + // a) An external repo within the hashicorp org - return external repo version with +builtin + // b) Within the Vault repo itself - return Vault version with +builtin.vault + // + // The repo names are predictable, but follow slightly different patterns + // for each plugin type. + t := pluginType.String() + switch pluginType { + case consts.PluginTypeDatabase: + // Database plugin built-ins are registered as e.g. "postgresql-database-plugin" + pluginName = strings.TrimSuffix(pluginName, "-database-plugin") + case consts.PluginTypeSecrets: + // Repos use "secrets", pluginType.String() is "secret". + t = "secrets" + } + pluginModulePath := fmt.Sprintf("github.com/hashicorp/vault-plugin-%s-%s", t, pluginName) + + for _, dep := range buildInfo.Deps { + if dep.Path == pluginModulePath { + return dep.Version + "+builtin" + } + } + + return DefaultBuiltinVersion +} diff --git a/http/handler_test.go b/http/handler_test.go index 117f77959..6fa607dc8 100644 --- a/http/handler_test.go +++ b/http/handler_test.go @@ -17,6 +17,7 @@ import ( "github.com/go-test/deep" "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/vault/helper/namespace" + "github.com/hashicorp/vault/helper/versions" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/vault" @@ -415,7 +416,7 @@ func TestSysMounts_headerAuth(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "sys/": map[string]interface{}{ @@ -433,7 +434,7 @@ func TestSysMounts_headerAuth(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, "version": "", }, "cubbyhole/": map[string]interface{}{ @@ -450,7 +451,7 @@ func TestSysMounts_headerAuth(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), "version": "", }, "identity/": map[string]interface{}{ @@ -468,7 +469,7 @@ func TestSysMounts_headerAuth(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), "version": "", }, }, @@ -486,7 +487,7 @@ func TestSysMounts_headerAuth(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "sys/": map[string]interface{}{ @@ -504,7 +505,7 @@ func TestSysMounts_headerAuth(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, "version": "", }, "cubbyhole/": map[string]interface{}{ @@ -521,7 +522,7 @@ func TestSysMounts_headerAuth(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), "version": "", }, "identity/": map[string]interface{}{ @@ -539,7 +540,7 @@ func TestSysMounts_headerAuth(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), "version": "", }, } diff --git a/http/sys_auth_test.go b/http/sys_auth_test.go index 53001b0f0..5499b7c6d 100644 --- a/http/sys_auth_test.go +++ b/http/sys_auth_test.go @@ -8,6 +8,8 @@ import ( "time" "github.com/go-test/deep" + "github.com/hashicorp/vault/helper/versions" + "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/vault" ) @@ -128,7 +130,7 @@ func TestSysEnableAuth(t *testing.T) { "options": map[string]interface{}{}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeCredential, "approle"), "version": "", }, "token/": map[string]interface{}{ @@ -166,7 +168,7 @@ func TestSysEnableAuth(t *testing.T) { "options": map[string]interface{}{}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeCredential, "approle"), "version": "", }, "token/": map[string]interface{}{ @@ -531,7 +533,7 @@ func TestSysRemountAuth(t *testing.T) { "options": map[string]interface{}{}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "token/": map[string]interface{}{ @@ -568,7 +570,7 @@ func TestSysRemountAuth(t *testing.T) { "options": map[string]interface{}{}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "token/": map[string]interface{}{ diff --git a/http/sys_mount_test.go b/http/sys_mount_test.go index dd3d22007..0aaf9c8f4 100644 --- a/http/sys_mount_test.go +++ b/http/sys_mount_test.go @@ -7,9 +7,10 @@ import ( "testing" "time" - "github.com/go-test/deep" - "github.com/fatih/structs" + "github.com/go-test/deep" + "github.com/hashicorp/vault/helper/versions" + "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/vault" ) @@ -44,7 +45,7 @@ func TestSysMounts(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "sys/": map[string]interface{}{ @@ -62,7 +63,7 @@ func TestSysMounts(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, "version": "", }, "cubbyhole/": map[string]interface{}{ @@ -79,7 +80,7 @@ func TestSysMounts(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), "version": "", }, "identity/": map[string]interface{}{ @@ -97,7 +98,7 @@ func TestSysMounts(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), "version": "", }, }, @@ -115,7 +116,7 @@ func TestSysMounts(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "sys/": map[string]interface{}{ @@ -133,7 +134,7 @@ func TestSysMounts(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, "version": "", }, "cubbyhole/": map[string]interface{}{ @@ -150,7 +151,7 @@ func TestSysMounts(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), "version": "", }, "identity/": map[string]interface{}{ @@ -168,7 +169,7 @@ func TestSysMounts(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), "version": "", }, } @@ -233,7 +234,7 @@ func TestSysMount(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "secret/": map[string]interface{}{ @@ -250,7 +251,7 @@ func TestSysMount(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "sys/": map[string]interface{}{ @@ -268,7 +269,7 @@ func TestSysMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, "version": "", }, "cubbyhole/": map[string]interface{}{ @@ -285,7 +286,7 @@ func TestSysMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), "version": "", }, "identity/": map[string]interface{}{ @@ -303,7 +304,7 @@ func TestSysMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), "version": "", }, }, @@ -321,7 +322,7 @@ func TestSysMount(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "secret/": map[string]interface{}{ @@ -338,7 +339,7 @@ func TestSysMount(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "sys/": map[string]interface{}{ @@ -356,7 +357,7 @@ func TestSysMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, "version": "", }, "cubbyhole/": map[string]interface{}{ @@ -373,7 +374,7 @@ func TestSysMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), "version": "", }, "identity/": map[string]interface{}{ @@ -391,7 +392,7 @@ func TestSysMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), "version": "", }, } @@ -491,7 +492,7 @@ func TestSysRemount(t *testing.T) { "options": map[string]interface{}{}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "secret/": map[string]interface{}{ @@ -508,7 +509,7 @@ func TestSysRemount(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "sys/": map[string]interface{}{ @@ -526,7 +527,7 @@ func TestSysRemount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, "version": "", }, "cubbyhole/": map[string]interface{}{ @@ -543,7 +544,7 @@ func TestSysRemount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), "version": "", }, "identity/": map[string]interface{}{ @@ -561,7 +562,7 @@ func TestSysRemount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), "version": "", }, }, @@ -579,7 +580,7 @@ func TestSysRemount(t *testing.T) { "options": map[string]interface{}{}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "secret/": map[string]interface{}{ @@ -596,7 +597,7 @@ func TestSysRemount(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "sys/": map[string]interface{}{ @@ -614,7 +615,7 @@ func TestSysRemount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, "version": "", }, "cubbyhole/": map[string]interface{}{ @@ -631,7 +632,7 @@ func TestSysRemount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), "version": "", }, "identity/": map[string]interface{}{ @@ -649,7 +650,7 @@ func TestSysRemount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), "version": "", }, } @@ -714,7 +715,7 @@ func TestSysUnmount(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "sys/": map[string]interface{}{ @@ -732,7 +733,7 @@ func TestSysUnmount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, "version": "", }, "cubbyhole/": map[string]interface{}{ @@ -749,7 +750,7 @@ func TestSysUnmount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), "version": "", }, "identity/": map[string]interface{}{ @@ -767,7 +768,7 @@ func TestSysUnmount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), "version": "", }, }, @@ -785,7 +786,7 @@ func TestSysUnmount(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "sys/": map[string]interface{}{ @@ -803,7 +804,7 @@ func TestSysUnmount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, "version": "", }, "cubbyhole/": map[string]interface{}{ @@ -820,7 +821,7 @@ func TestSysUnmount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), "version": "", }, "identity/": map[string]interface{}{ @@ -838,7 +839,7 @@ func TestSysUnmount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), "version": "", }, } @@ -989,7 +990,7 @@ func TestSysTuneMount(t *testing.T) { "options": map[string]interface{}{}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "secret/": map[string]interface{}{ @@ -1006,7 +1007,7 @@ func TestSysTuneMount(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "sys/": map[string]interface{}{ @@ -1024,7 +1025,7 @@ func TestSysTuneMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, "version": "", }, "cubbyhole/": map[string]interface{}{ @@ -1041,7 +1042,7 @@ func TestSysTuneMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), "version": "", }, "identity/": map[string]interface{}{ @@ -1059,7 +1060,7 @@ func TestSysTuneMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), "version": "", }, }, @@ -1077,7 +1078,7 @@ func TestSysTuneMount(t *testing.T) { "options": map[string]interface{}{}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "secret/": map[string]interface{}{ @@ -1094,7 +1095,7 @@ func TestSysTuneMount(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "sys/": map[string]interface{}{ @@ -1112,7 +1113,7 @@ func TestSysTuneMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, "version": "", }, "cubbyhole/": map[string]interface{}{ @@ -1129,7 +1130,7 @@ func TestSysTuneMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), "version": "", }, "identity/": map[string]interface{}{ @@ -1147,7 +1148,7 @@ func TestSysTuneMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), "version": "", }, } @@ -1238,7 +1239,7 @@ func TestSysTuneMount(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "secret/": map[string]interface{}{ @@ -1255,7 +1256,7 @@ func TestSysTuneMount(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "sys/": map[string]interface{}{ @@ -1273,7 +1274,7 @@ func TestSysTuneMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, "version": "", }, "cubbyhole/": map[string]interface{}{ @@ -1290,7 +1291,7 @@ func TestSysTuneMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), "version": "", }, "identity/": map[string]interface{}{ @@ -1308,7 +1309,7 @@ func TestSysTuneMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), "version": "", }, }, @@ -1326,7 +1327,7 @@ func TestSysTuneMount(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "secret/": map[string]interface{}{ @@ -1343,7 +1344,7 @@ func TestSysTuneMount(t *testing.T) { "options": map[string]interface{}{"version": "1"}, "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "sys/": map[string]interface{}{ @@ -1361,7 +1362,7 @@ func TestSysTuneMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), "version": "", }, "cubbyhole/": map[string]interface{}{ @@ -1378,7 +1379,7 @@ func TestSysTuneMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), "version": "", }, "identity/": map[string]interface{}{ @@ -1396,7 +1397,7 @@ func TestSysTuneMount(t *testing.T) { "options": interface{}(nil), "sha": "", "running_sha": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), "version": "", }, } diff --git a/sdk/database/dbplugin/v5/grpc_client.go b/sdk/database/dbplugin/v5/grpc_client.go index e71c79794..cfddfcd57 100644 --- a/sdk/database/dbplugin/v5/grpc_client.go +++ b/sdk/database/dbplugin/v5/grpc_client.go @@ -9,17 +9,28 @@ import ( "github.com/golang/protobuf/ptypes" "github.com/hashicorp/vault/sdk/database/dbplugin/v5/proto" "github.com/hashicorp/vault/sdk/helper/pluginutil" + "github.com/hashicorp/vault/sdk/logical" ) var ( - _ Database = gRPCClient{} + _ Database = gRPCClient{} + _ logical.PluginVersioner = gRPCClient{} ErrPluginShutdown = errors.New("plugin shutdown") ) type gRPCClient struct { - client proto.DatabaseClient - doneCtx context.Context + client proto.DatabaseClient + versionClient logical.PluginVersionClient + doneCtx context.Context +} + +func (c gRPCClient) PluginVersion() logical.PluginVersion { + version, _ := c.versionClient.Version(context.Background(), &logical.Empty{}) + if version != nil { + return logical.PluginVersion{Version: version.PluginVersion} + } + return logical.EmptyPluginVersion } func (c gRPCClient) Initialize(ctx context.Context, req InitializeRequest) (InitializeResponse, error) { diff --git a/sdk/database/dbplugin/v5/grpc_database_plugin.go b/sdk/database/dbplugin/v5/grpc_database_plugin.go index 92ed2dcc6..441030df9 100644 --- a/sdk/database/dbplugin/v5/grpc_database_plugin.go +++ b/sdk/database/dbplugin/v5/grpc_database_plugin.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/go-plugin" "github.com/hashicorp/vault/sdk/database/dbplugin/v5/proto" "github.com/hashicorp/vault/sdk/helper/pluginutil" + "github.com/hashicorp/vault/sdk/logical" "google.golang.org/grpc" ) @@ -54,13 +55,15 @@ func (d GRPCDatabasePlugin) GRPCServer(_ *plugin.GRPCBroker, s *grpc.Server) err } proto.RegisterDatabaseServer(s, &server) + logical.RegisterPluginVersionServer(s, &server) return nil } func (GRPCDatabasePlugin) GRPCClient(doneCtx context.Context, _ *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { client := gRPCClient{ - client: proto.NewDatabaseClient(c), - doneCtx: doneCtx, + client: proto.NewDatabaseClient(c), + versionClient: logical.NewPluginVersionClient(c), + doneCtx: doneCtx, } return client, nil } diff --git a/sdk/database/dbplugin/v5/grpc_server.go b/sdk/database/dbplugin/v5/grpc_server.go index d38e97127..b8fc7b672 100644 --- a/sdk/database/dbplugin/v5/grpc_server.go +++ b/sdk/database/dbplugin/v5/grpc_server.go @@ -9,6 +9,7 @@ import ( "github.com/golang/protobuf/ptypes" "github.com/hashicorp/vault/sdk/database/dbplugin/v5/proto" "github.com/hashicorp/vault/sdk/helper/pluginutil" + "github.com/hashicorp/vault/sdk/logical" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -17,6 +18,7 @@ var _ proto.DatabaseServer = &gRPCServer{} type gRPCServer struct { proto.UnimplementedDatabaseServer + logical.UnimplementedPluginVersionServer // holds the non-multiplexed Database // when this is set the plugin does not support multiplexing @@ -302,6 +304,18 @@ func (g *gRPCServer) Close(ctx context.Context, _ *proto.Empty) (*proto.Empty, e return &proto.Empty{}, nil } +// Version forwards the version request to the underlying Database implementation. +func (g *gRPCServer) Version(ctx context.Context, _ *logical.Empty) (*logical.VersionReply, error) { + impl, err := g.getDatabaseInternal(ctx) + if err != nil { + return nil, err + } + if versioner, ok := impl.(logical.PluginVersioner); ok { + return &logical.VersionReply{PluginVersion: versioner.PluginVersion().Version}, nil + } + return &logical.VersionReply{}, nil +} + func getStatementsFromProto(protoStmts *proto.Statements) (statements Statements) { if protoStmts == nil { return statements diff --git a/sdk/database/dbplugin/v5/grpc_server_test.go b/sdk/database/dbplugin/v5/grpc_server_test.go index b901839d0..7399bf557 100644 --- a/sdk/database/dbplugin/v5/grpc_server_test.go +++ b/sdk/database/dbplugin/v5/grpc_server_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/hashicorp/vault/sdk/logical" "google.golang.org/protobuf/types/known/structpb" "github.com/golang/protobuf/ptypes" @@ -581,6 +582,55 @@ func TestGRPCServer_Close(t *testing.T) { } } +func TestGRPCServer_Version(t *testing.T) { + type testCase struct { + db Database + expectedResp string + expectErr bool + expectCode codes.Code + } + + tests := map[string]testCase{ + "backend that does not implement version": { + db: fakeDatabase{}, + expectedResp: "", + expectErr: false, + expectCode: codes.OK, + }, + "backend with version": { + db: fakeDatabaseWithVersion{ + version: "v123", + }, + expectedResp: "v123", + expectErr: false, + expectCode: codes.OK, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + idCtx, g := testGrpcServer(t, test.db) + resp, err := g.Version(idCtx, &logical.Empty{}) + + if test.expectErr && err == nil { + t.Fatalf("err expected, got nil") + } + if !test.expectErr && err != nil { + t.Fatalf("no error expected, got: %s", err) + } + + actualCode := status.Code(err) + if actualCode != test.expectCode { + t.Fatalf("Actual code: %s Expected code: %s", actualCode, test.expectCode) + } + + if !reflect.DeepEqual(resp.PluginVersion, test.expectedResp) { + t.Fatalf("Actual response: %#v\nExpected response: %#v", resp, test.expectedResp) + } + }) + } +} + // testGrpcServer is a test helper that returns a context with an ID set in its // metadata and a gRPCServer instance for a multiplexed plugin func testGrpcServer(t *testing.T, db Database) (context.Context, gRPCServer) { @@ -747,3 +797,40 @@ func (f *recordingDatabase) Close() error { } return f.next.Close() } + +type fakeDatabaseWithVersion struct { + version string +} + +func (e fakeDatabaseWithVersion) PluginVersion() logical.PluginVersion { + return logical.PluginVersion{Version: e.version} +} + +func (e fakeDatabaseWithVersion) Initialize(_ context.Context, _ InitializeRequest) (InitializeResponse, error) { + return InitializeResponse{}, nil +} + +func (e fakeDatabaseWithVersion) NewUser(_ context.Context, _ NewUserRequest) (NewUserResponse, error) { + return NewUserResponse{}, nil +} + +func (e fakeDatabaseWithVersion) UpdateUser(_ context.Context, _ UpdateUserRequest) (UpdateUserResponse, error) { + return UpdateUserResponse{}, nil +} + +func (e fakeDatabaseWithVersion) DeleteUser(_ context.Context, _ DeleteUserRequest) (DeleteUserResponse, error) { + return DeleteUserResponse{}, nil +} + +func (e fakeDatabaseWithVersion) Type() (string, error) { + return "", nil +} + +func (e fakeDatabaseWithVersion) Close() error { + return nil +} + +var ( + _ Database = (*fakeDatabaseWithVersion)(nil) + _ logical.PluginVersioner = (*fakeDatabaseWithVersion)(nil) +) diff --git a/sdk/database/dbplugin/v5/middleware.go b/sdk/database/dbplugin/v5/middleware.go index 164094efe..3c1a85a28 100644 --- a/sdk/database/dbplugin/v5/middleware.go +++ b/sdk/database/dbplugin/v5/middleware.go @@ -7,9 +7,10 @@ import ( "strings" "time" - metrics "github.com/armon/go-metrics" + "github.com/armon/go-metrics" "github.com/hashicorp/errwrap" log "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/sdk/logical" "google.golang.org/grpc/status" ) @@ -17,7 +18,10 @@ import ( // Tracing Middleware // /////////////////////////////////////////////////// -var _ Database = databaseTracingMiddleware{} +var ( + _ Database = databaseTracingMiddleware{} + _ logical.PluginVersioner = databaseTracingMiddleware{} +) // databaseTracingMiddleware wraps a implementation of Database and executes // trace logging on function call. @@ -26,6 +30,21 @@ type databaseTracingMiddleware struct { logger log.Logger } +func (mw databaseTracingMiddleware) PluginVersion() (resp logical.PluginVersion) { + defer func(then time.Time) { + mw.logger.Trace("version", + "status", "finished", + "version", resp, + "took", time.Since(then)) + }(time.Now()) + + mw.logger.Trace("version", "status", "started") + if versioner, ok := mw.next.(logical.PluginVersioner); ok { + return versioner.PluginVersion() + } + return logical.EmptyPluginVersion +} + func (mw databaseTracingMiddleware) Initialize(ctx context.Context, req InitializeRequest) (resp InitializeResponse, err error) { defer func(then time.Time) { mw.logger.Trace("initialize", @@ -98,7 +117,10 @@ func (mw databaseTracingMiddleware) Close() (err error) { // Metrics Middleware Domain // /////////////////////////////////////////////////// -var _ Database = databaseMetricsMiddleware{} +var ( + _ Database = databaseMetricsMiddleware{} + _ logical.PluginVersioner = databaseMetricsMiddleware{} +) // databaseMetricsMiddleware wraps an implementation of Databases and on // function call logs metrics about this instance. @@ -108,6 +130,21 @@ type databaseMetricsMiddleware struct { typeStr string } +func (mw databaseMetricsMiddleware) PluginVersion() logical.PluginVersion { + defer func(now time.Time) { + metrics.MeasureSince([]string{"database", "PluginVersion"}, now) + metrics.MeasureSince([]string{"database", mw.typeStr, "PluginVersion"}, now) + }(time.Now()) + + metrics.IncrCounter([]string{"database", "PluginVersion"}, 1) + metrics.IncrCounter([]string{"database", mw.typeStr, "PluginVersion"}, 1) + + if versioner, ok := mw.next.(logical.PluginVersioner); ok { + return versioner.PluginVersion() + } + return logical.EmptyPluginVersion +} + func (mw databaseMetricsMiddleware) Initialize(ctx context.Context, req InitializeRequest) (resp InitializeResponse, err error) { defer func(now time.Time) { metrics.MeasureSince([]string{"database", "Initialize"}, now) diff --git a/sdk/database/dbplugin/v5/plugin_client.go b/sdk/database/dbplugin/v5/plugin_client.go index 4ef24cb8e..caea00a8f 100644 --- a/sdk/database/dbplugin/v5/plugin_client.go +++ b/sdk/database/dbplugin/v5/plugin_client.go @@ -4,16 +4,26 @@ import ( "context" "errors" - plugin "github.com/hashicorp/go-plugin" + "github.com/hashicorp/go-plugin" "github.com/hashicorp/vault/sdk/database/dbplugin/v5/proto" "github.com/hashicorp/vault/sdk/helper/pluginutil" + "github.com/hashicorp/vault/sdk/logical" ) +var _ logical.PluginVersioner = (*DatabasePluginClient)(nil) + type DatabasePluginClient struct { client pluginutil.PluginClient Database } +func (dc *DatabasePluginClient) PluginVersion() logical.PluginVersion { + if versioner, ok := dc.Database.(logical.PluginVersioner); ok { + return versioner.PluginVersion() + } + return logical.EmptyPluginVersion +} + // This wraps the Close call and ensures we both close the database connection // and kill the plugin. func (dc *DatabasePluginClient) Close() error { @@ -55,6 +65,7 @@ func NewPluginClient(ctx context.Context, sys pluginutil.RunnerUtil, config plug // This is an abstraction leak from go-plugin but it is necessary in // order to enable multiplexing on multiplexed plugins c.client = proto.NewDatabaseClient(pluginClient.Conn()) + c.versionClient = logical.NewPluginVersionClient(pluginClient.Conn()) db = c default: diff --git a/sdk/database/dbplugin/v5/plugin_client_test.go b/sdk/database/dbplugin/v5/plugin_client_test.go index c8faf55e1..f41ecf8ef 100644 --- a/sdk/database/dbplugin/v5/plugin_client_test.go +++ b/sdk/database/dbplugin/v5/plugin_client_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/pluginutil" "github.com/hashicorp/vault/sdk/helper/wrapping" + "github.com/hashicorp/vault/sdk/logical" "github.com/stretchr/testify/mock" "google.golang.org/grpc" ) @@ -38,7 +39,7 @@ func TestNewPluginClient(t *testing.T) { dispenseResp: gRPCClient{client: fakeClient{}}, dispenseErr: nil, }, - Database: gRPCClient{proto.NewDatabaseClient(nil), context.Context(nil)}, + Database: gRPCClient{client: proto.NewDatabaseClient(nil), versionClient: logical.NewPluginVersionClient(nil), doneCtx: context.Context(nil)}, }, expectedErr: nil, }, diff --git a/sdk/database/dbplugin/v5/proto/database.proto b/sdk/database/dbplugin/v5/proto/database.proto index c5d4c5562..b4959f709 100644 --- a/sdk/database/dbplugin/v5/proto/database.proto +++ b/sdk/database/dbplugin/v5/proto/database.proto @@ -101,4 +101,4 @@ service Database { rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse); rpc Type(Empty) returns (TypeResponse); rpc Close(Empty) returns (Empty); -} +} \ No newline at end of file diff --git a/sdk/framework/backend.go b/sdk/framework/backend.go index 3fe74fa19..d42662c3f 100644 --- a/sdk/framework/backend.go +++ b/sdk/framework/backend.go @@ -91,6 +91,9 @@ type Backend struct { // BackendType is the logical.BackendType for the backend implementation BackendType logical.BackendType + // RunningVersion is the optional version that will be self-reported + RunningVersion string + logger log.Logger system logical.SystemView once sync.Once @@ -428,6 +431,13 @@ func (b *Backend) Type() logical.BackendType { return b.BackendType } +// Version returns the plugin version information +func (b *Backend) PluginVersion() logical.PluginVersion { + return logical.PluginVersion{ + Version: b.RunningVersion, + } +} + // Route looks up the path that would be used for a given path string. func (b *Backend) Route(path string) *Path { result, _ := b.route(path) diff --git a/sdk/logical/logical.go b/sdk/logical/logical.go index fb9619ae2..601148952 100644 --- a/sdk/logical/logical.go +++ b/sdk/logical/logical.go @@ -137,3 +137,20 @@ type Auditor interface { AuditRequest(ctx context.Context, input *LogInput) error AuditResponse(ctx context.Context, input *LogInput) error } + +// Externaler allows us to check if a backend is running externally (i.e., over GRPC) +type Externaler interface { + IsExternal() bool +} + +type PluginVersion struct { + Version string +} + +// PluginVersioner is an optional interface to return version info. +type PluginVersioner interface { + // PluginVersion returns the version for the backend + PluginVersion() PluginVersion +} + +var EmptyPluginVersion = PluginVersion{""} diff --git a/sdk/logical/version.pb.go b/sdk/logical/version.pb.go new file mode 100644 index 000000000..b054cf0ee --- /dev/null +++ b/sdk/logical/version.pb.go @@ -0,0 +1,204 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.0 +// protoc v3.21.5 +// source: sdk/logical/version.proto + +package logical + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Empty struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *Empty) Reset() { + *x = Empty{} + if protoimpl.UnsafeEnabled { + mi := &file_sdk_logical_version_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Empty) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Empty) ProtoMessage() {} + +func (x *Empty) ProtoReflect() protoreflect.Message { + mi := &file_sdk_logical_version_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Empty.ProtoReflect.Descriptor instead. +func (*Empty) Descriptor() ([]byte, []int) { + return file_sdk_logical_version_proto_rawDescGZIP(), []int{0} +} + +// VersionReply is the reply for the Version method. +type VersionReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PluginVersion string `protobuf:"bytes,1,opt,name=plugin_version,json=pluginVersion,proto3" json:"plugin_version,omitempty"` +} + +func (x *VersionReply) Reset() { + *x = VersionReply{} + if protoimpl.UnsafeEnabled { + mi := &file_sdk_logical_version_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VersionReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VersionReply) ProtoMessage() {} + +func (x *VersionReply) ProtoReflect() protoreflect.Message { + mi := &file_sdk_logical_version_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VersionReply.ProtoReflect.Descriptor instead. +func (*VersionReply) Descriptor() ([]byte, []int) { + return file_sdk_logical_version_proto_rawDescGZIP(), []int{1} +} + +func (x *VersionReply) GetPluginVersion() string { + if x != nil { + return x.PluginVersion + } + return "" +} + +var File_sdk_logical_version_proto protoreflect.FileDescriptor + +var file_sdk_logical_version_proto_rawDesc = []byte{ + 0x0a, 0x19, 0x73, 0x64, 0x6b, 0x2f, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x61, 0x6c, 0x2f, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x6c, 0x6f, 0x67, + 0x69, 0x63, 0x61, 0x6c, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x35, 0x0a, + 0x0c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x25, 0x0a, + 0x0e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x32, 0x41, 0x0a, 0x0d, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x0e, 0x2e, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x61, 0x6c, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x1a, 0x15, 0x2e, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x61, 0x6c, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x42, 0x28, 0x5a, 0x26, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, + 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x61, + 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_sdk_logical_version_proto_rawDescOnce sync.Once + file_sdk_logical_version_proto_rawDescData = file_sdk_logical_version_proto_rawDesc +) + +func file_sdk_logical_version_proto_rawDescGZIP() []byte { + file_sdk_logical_version_proto_rawDescOnce.Do(func() { + file_sdk_logical_version_proto_rawDescData = protoimpl.X.CompressGZIP(file_sdk_logical_version_proto_rawDescData) + }) + return file_sdk_logical_version_proto_rawDescData +} + +var file_sdk_logical_version_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_sdk_logical_version_proto_goTypes = []interface{}{ + (*Empty)(nil), // 0: logical.Empty + (*VersionReply)(nil), // 1: logical.VersionReply +} +var file_sdk_logical_version_proto_depIdxs = []int32{ + 0, // 0: logical.PluginVersion.Version:input_type -> logical.Empty + 1, // 1: logical.PluginVersion.Version:output_type -> logical.VersionReply + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_sdk_logical_version_proto_init() } +func file_sdk_logical_version_proto_init() { + if File_sdk_logical_version_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_sdk_logical_version_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Empty); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_sdk_logical_version_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VersionReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_sdk_logical_version_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_sdk_logical_version_proto_goTypes, + DependencyIndexes: file_sdk_logical_version_proto_depIdxs, + MessageInfos: file_sdk_logical_version_proto_msgTypes, + }.Build() + File_sdk_logical_version_proto = out.File + file_sdk_logical_version_proto_rawDesc = nil + file_sdk_logical_version_proto_goTypes = nil + file_sdk_logical_version_proto_depIdxs = nil +} diff --git a/sdk/logical/version.proto b/sdk/logical/version.proto new file mode 100644 index 000000000..345051ae9 --- /dev/null +++ b/sdk/logical/version.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; +package logical; + +option go_package = "github.com/hashicorp/vault/sdk/logical"; + +message Empty {} + +// VersionReply is the reply for the Version method. +message VersionReply { + string plugin_version = 1; +} + +// PluginVersion is an optional RPC service implemented by plugins. +service PluginVersion { + // Version returns version information for the plugin. + rpc Version(Empty) returns (VersionReply); +} \ No newline at end of file diff --git a/sdk/logical/version_grpc.pb.go b/sdk/logical/version_grpc.pb.go new file mode 100644 index 000000000..a69e97059 --- /dev/null +++ b/sdk/logical/version_grpc.pb.go @@ -0,0 +1,103 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package logical + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// PluginVersionClient is the client API for PluginVersion service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type PluginVersionClient interface { + // Version returns version information for the plugin. + Version(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VersionReply, error) +} + +type pluginVersionClient struct { + cc grpc.ClientConnInterface +} + +func NewPluginVersionClient(cc grpc.ClientConnInterface) PluginVersionClient { + return &pluginVersionClient{cc} +} + +func (c *pluginVersionClient) Version(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VersionReply, error) { + out := new(VersionReply) + err := c.cc.Invoke(ctx, "/logical.PluginVersion/Version", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// PluginVersionServer is the server API for PluginVersion service. +// All implementations must embed UnimplementedPluginVersionServer +// for forward compatibility +type PluginVersionServer interface { + // Version returns version information for the plugin. + Version(context.Context, *Empty) (*VersionReply, error) + mustEmbedUnimplementedPluginVersionServer() +} + +// UnimplementedPluginVersionServer must be embedded to have forward compatible implementations. +type UnimplementedPluginVersionServer struct { +} + +func (UnimplementedPluginVersionServer) Version(context.Context, *Empty) (*VersionReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Version not implemented") +} +func (UnimplementedPluginVersionServer) mustEmbedUnimplementedPluginVersionServer() {} + +// UnsafePluginVersionServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to PluginVersionServer will +// result in compilation errors. +type UnsafePluginVersionServer interface { + mustEmbedUnimplementedPluginVersionServer() +} + +func RegisterPluginVersionServer(s grpc.ServiceRegistrar, srv PluginVersionServer) { + s.RegisterService(&PluginVersion_ServiceDesc, srv) +} + +func _PluginVersion_Version_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginVersionServer).Version(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/logical.PluginVersion/Version", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginVersionServer).Version(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +// PluginVersion_ServiceDesc is the grpc.ServiceDesc for PluginVersion service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var PluginVersion_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "logical.PluginVersion", + HandlerType: (*PluginVersionServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Version", + Handler: _PluginVersion_Version_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "sdk/logical/version.proto", +} diff --git a/sdk/plugin/backend.go b/sdk/plugin/backend.go index 545565684..8bfa97800 100644 --- a/sdk/plugin/backend.go +++ b/sdk/plugin/backend.go @@ -7,7 +7,7 @@ import ( "google.golang.org/grpc" log "github.com/hashicorp/go-hclog" - plugin "github.com/hashicorp/go-plugin" + "github.com/hashicorp/go-plugin" "github.com/hashicorp/vault/sdk/helper/pluginutil" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/plugin/pb" @@ -51,17 +51,19 @@ func (b GRPCBackendPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) } pb.RegisterBackendServer(s, &server) + logical.RegisterPluginVersionServer(s, &server) return nil } 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, - metadataMode: b.MetadataMode, + client: pb.NewBackendClient(c), + versionClient: logical.NewPluginVersionClient(c), + clientConn: c, + broker: broker, + cleanupCh: make(chan struct{}), + doneCtx: ctx, + metadataMode: b.MetadataMode, } // Create the value and set the type diff --git a/sdk/plugin/grpc_backend_client.go b/sdk/plugin/grpc_backend_client.go index 9ea3c23f8..51cdaf174 100644 --- a/sdk/plugin/grpc_backend_client.go +++ b/sdk/plugin/grpc_backend_client.go @@ -6,15 +6,14 @@ import ( "math" "sync/atomic" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - log "github.com/hashicorp/go-hclog" - plugin "github.com/hashicorp/go-plugin" + "github.com/hashicorp/go-plugin" "github.com/hashicorp/vault/sdk/helper/pluginutil" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/plugin/pb" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) var ( @@ -28,9 +27,10 @@ var _ logical.Backend = &backendGRPCPluginClient{} // backendPluginClient implements logical.Backend and is the // go-plugin client. type backendGRPCPluginClient struct { - broker *plugin.GRPCBroker - client pb.BackendClient - metadataMode bool + broker *plugin.GRPCBroker + client pb.BackendClient + versionClient logical.PluginVersionClient + metadataMode bool system logical.SystemView logger log.Logger @@ -280,3 +280,23 @@ func (b *backendGRPCPluginClient) Type() logical.BackendType { return logical.BackendType(reply.Type) } + +func (b *backendGRPCPluginClient) PluginVersion() logical.PluginVersion { + reply, err := b.versionClient.Version(b.doneCtx, &logical.Empty{}) + if err != nil { + if stErr, ok := status.FromError(err); ok { + if stErr.Code() == codes.Unimplemented { + return logical.EmptyPluginVersion + } + } + b.Logger().Warn("Unknown error getting plugin version", "err", err) + return logical.EmptyPluginVersion + } + return logical.PluginVersion{ + Version: reply.GetPluginVersion(), + } +} + +func (b *backendGRPCPluginClient) IsExternal() bool { + return true +} diff --git a/sdk/plugin/grpc_backend_server.go b/sdk/plugin/grpc_backend_server.go index 3d38a0e26..52b3853ed 100644 --- a/sdk/plugin/grpc_backend_server.go +++ b/sdk/plugin/grpc_backend_server.go @@ -7,7 +7,7 @@ import ( "sync" log "github.com/hashicorp/go-hclog" - plugin "github.com/hashicorp/go-plugin" + "github.com/hashicorp/go-plugin" "github.com/hashicorp/vault/sdk/helper/pluginutil" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/plugin/pb" @@ -27,6 +27,7 @@ type backendInstance struct { type backendGRPCPluginServer struct { pb.UnimplementedBackendServer + logical.UnimplementedPluginVersionServer broker *plugin.GRPCBroker @@ -266,3 +267,19 @@ func (b *backendGRPCPluginServer) Type(ctx context.Context, _ *pb.Empty) (*pb.Ty Type: uint32(backend.Type()), }, nil } + +func (b *backendGRPCPluginServer) Version(ctx context.Context, _ *logical.Empty) (*logical.VersionReply, error) { + backend, _, err := b.getBackendAndBrokeredClient(ctx) + if err != nil { + return &logical.VersionReply{}, err + } + + if versioner, ok := backend.(logical.PluginVersioner); ok { + return &logical.VersionReply{ + PluginVersion: versioner.PluginVersion().Version, + }, nil + } + return &logical.VersionReply{ + PluginVersion: "", + }, nil +} diff --git a/sdk/plugin/grpc_backend_test.go b/sdk/plugin/grpc_backend_test.go index e332a9c22..5ab99fd42 100644 --- a/sdk/plugin/grpc_backend_test.go +++ b/sdk/plugin/grpc_backend_test.go @@ -147,6 +147,21 @@ func TestGRPCBackendPlugin_Initialize(t *testing.T) { } } +func TestGRPCBackendPlugin_Version(t *testing.T) { + b, cleanup := testGRPCBackend(t) + defer cleanup() + + versioner, ok := b.(logical.PluginVersioner) + if !ok { + t.Fatalf("Expected %T to implement logical.PluginVersioner interface", b) + } + + version := versioner.PluginVersion().Version + if version != "mock" { + t.Fatalf("Got version %s, expected 'mock'", version) + } +} + func testGRPCBackend(t *testing.T) (logical.Backend, func()) { // Create a mock provider pluginMap := map[string]gplugin.Plugin{ diff --git a/sdk/plugin/middleware.go b/sdk/plugin/middleware.go index 3f0babde2..546584ccc 100644 --- a/sdk/plugin/middleware.go +++ b/sdk/plugin/middleware.go @@ -98,3 +98,15 @@ func (b *BackendTracingMiddleware) Type() logical.BackendType { b.logger.Trace("type", "status", "started") return b.next.Type() } + +func (b *BackendTracingMiddleware) PluginVersion() logical.PluginVersion { + defer func(then time.Time) { + b.logger.Trace("version", "status", "finished", "took", time.Since(then)) + }(time.Now()) + + b.logger.Trace("version", "status", "started") + if versioner, ok := b.next.(logical.PluginVersioner); ok { + return versioner.PluginVersion() + } + return logical.EmptyPluginVersion +} diff --git a/sdk/plugin/mock/backend.go b/sdk/plugin/mock/backend.go index a5e8b5622..fc840809c 100644 --- a/sdk/plugin/mock/backend.go +++ b/sdk/plugin/mock/backend.go @@ -59,6 +59,7 @@ func Backend() *backend { BackendType: logical.TypeLogical, } b.internal = "bar" + b.RunningVersion = "mock" return &b } diff --git a/sdk/plugin/plugin.go b/sdk/plugin/plugin.go index fafa684a6..edbffcd69 100644 --- a/sdk/plugin/plugin.go +++ b/sdk/plugin/plugin.go @@ -143,3 +143,22 @@ func NewPluginClient(ctx context.Context, sys pluginutil.RunnerUtil, pluginRunne Backend: backend, }, nil } + +func (b *BackendPluginClient) PluginVersion() logical.PluginVersion { + if versioner, ok := b.Backend.(logical.PluginVersioner); ok { + return versioner.PluginVersion() + } + return logical.EmptyPluginVersion +} + +func (b *BackendPluginClient) IsExternal() bool { + if externaler, ok := b.Backend.(logical.Externaler); ok { + return externaler.IsExternal() + } + return true // default to true since this is only used for GRPC plugins +} + +var ( + _ logical.PluginVersioner = (*BackendPluginClient)(nil) + _ logical.Externaler = (*BackendPluginClient)(nil) +) diff --git a/sdk/plugin/plugin_v5.go b/sdk/plugin/plugin_v5.go index 8c37f74dc..2adf020a4 100644 --- a/sdk/plugin/plugin_v5.go +++ b/sdk/plugin/plugin_v5.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - 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" @@ -41,6 +41,22 @@ func (b *BackendPluginClientV5) Cleanup(ctx context.Context) { b.client.Reload() } +func (b *BackendPluginClientV5) IsExternal() bool { + return true +} + +func (b *BackendPluginClientV5) PluginVersion() logical.PluginVersion { + if versioner, ok := b.Backend.(logical.PluginVersioner); ok { + return versioner.PluginVersion() + } + return logical.EmptyPluginVersion +} + +var ( + _ logical.PluginVersioner = (*BackendPluginClientV5)(nil) + _ logical.Externaler = (*BackendPluginClientV5)(nil) +) + // NewBackendV5 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 @@ -111,6 +127,7 @@ func Dispense(rpcClient plugin.ClientProtocol, pluginClient pluginutil.PluginCli // This is an abstraction leak from go-plugin but it is necessary in // order to enable multiplexing on multiplexed plugins c.client = pb.NewBackendClient(pluginClient.Conn()) + c.versionClient = logical.NewPluginVersionClient(pluginClient.Conn()) backend = c default: @@ -144,6 +161,7 @@ func NewPluginClientV5(ctx context.Context, sys pluginutil.RunnerUtil, config pl // This is an abstraction leak from go-plugin but it is necessary in // order to enable multiplexing on multiplexed plugins c.client = pb.NewBackendClient(pluginClient.Conn()) + c.versionClient = logical.NewPluginVersionClient(pluginClient.Conn()) backend = c transport = "gRPC" diff --git a/vault/auth.go b/vault/auth.go index feaff927c..b37bb1b0c 100644 --- a/vault/auth.go +++ b/vault/auth.go @@ -7,9 +7,10 @@ import ( "strings" "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/namespace" + "github.com/hashicorp/vault/helper/versions" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/jsonutil" "github.com/hashicorp/vault/sdk/logical" @@ -182,7 +183,16 @@ func (c *Core) enableCredentialInternal(ctx context.Context, entry *MountEntry, if backendType != logical.TypeCredential { return fmt.Errorf("cannot mount %q of type %q as an auth backend", entry.Type, backendType) } - + // update the entry running version with the backend's reported version + if versioner, ok := backend.(logical.PluginVersioner); ok { + entry.RunningVersion = versioner.PluginVersion().Version + } + if entry.RunningVersion == "" { + // don't set the running version to a builtin if it is running as an external plugin + if externaler, ok := backend.(logical.Externaler); !ok || !externaler.IsExternal() { + entry.RunningVersion = versions.GetBuiltinVersion(consts.PluginTypeCredential, entry.Type) + } + } addPathCheckers(c, entry, backend, viewPath) // If the mount is filtered or we are on a DR secondary we don't want to diff --git a/vault/external_plugin_test.go b/vault/external_plugin_test.go index 09768241c..29b665d8f 100644 --- a/vault/external_plugin_test.go +++ b/vault/external_plugin_test.go @@ -242,6 +242,11 @@ func TestCore_EnableExternalPlugin_MultipleVersions(t *testing.T) { if raw.(*routeEntry).mountEntry.Version != tc.mountVersion { t.Errorf("Expected mount to be version %s but got %s", tc.mountVersion, raw.(*routeEntry).mountEntry.Version) } + + // we don't override the running version of non-builtins, and they don't have the version set explicitly (yet) + if raw.(*routeEntry).mountEntry.RunningVersion != "" { + t.Errorf("Expected mount to have no running version but got %s", raw.(*routeEntry).mountEntry.RunningVersion) + } }) } } diff --git a/vault/identity_store.go b/vault/identity_store.go index 1f43d9e6e..5dc32e306 100644 --- a/vault/identity_store.go +++ b/vault/identity_store.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/vault/helper/metricsutil" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/helper/storagepacker" + "github.com/hashicorp/vault/helper/versions" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/logical" @@ -112,6 +113,7 @@ func NewIdentityStore(ctx context.Context, core *Core, config *logical.BackendCo return nil }, + RunningVersion: versions.DefaultBuiltinVersion, } iStore.oidcCache = newOIDCCache(cache.NoExpiration, cache.NoExpiration) diff --git a/vault/logical_cubbyhole.go b/vault/logical_cubbyhole.go index 51d5e81a3..4f869747c 100644 --- a/vault/logical_cubbyhole.go +++ b/vault/logical_cubbyhole.go @@ -6,7 +6,9 @@ import ( "fmt" "strings" + "github.com/hashicorp/vault/helper/versions" "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/jsonutil" "github.com/hashicorp/vault/sdk/logical" ) @@ -15,7 +17,8 @@ import ( func CubbyholeBackendFactory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { b := &CubbyholeBackend{} b.Backend = &framework.Backend{ - Help: strings.TrimSpace(cubbyholeHelp), + Help: strings.TrimSpace(cubbyholeHelp), + RunningVersion: versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), } b.Backend.Paths = append(b.Backend.Paths, b.paths()...) diff --git a/vault/logical_system.go b/vault/logical_system.go index 4373083b4..743482c77 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -21,6 +21,7 @@ import ( "time" "unicode" + "github.com/hashicorp/vault/helper/versions" "golang.org/x/crypto/sha3" "github.com/hashicorp/errwrap" @@ -84,7 +85,8 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend { } b.Backend = &framework.Backend{ - Help: strings.TrimSpace(sysHelpRoot), + RunningVersion: versions.DefaultBuiltinVersion, + Help: strings.TrimSpace(sysHelpRoot), PathsSpecial: &logical.Paths{ Root: []string{ diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index b8dce2690..3eea465fe 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/helper/random" + "github.com/hashicorp/vault/helper/versions" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/compressutil" "github.com/hashicorp/vault/sdk/helper/consts" @@ -175,7 +176,7 @@ func TestSystemBackend_mounts(t *testing.T) { "sha": "", "running_sha": "", "version": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), }, "sys/": map[string]interface{}{ "type": "system", @@ -195,7 +196,7 @@ func TestSystemBackend_mounts(t *testing.T) { "sha": "", "running_sha": "", "version": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -214,7 +215,7 @@ func TestSystemBackend_mounts(t *testing.T) { "sha": "", "running_sha": "", "version": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), }, "identity/": map[string]interface{}{ "description": "identity store", @@ -234,7 +235,7 @@ func TestSystemBackend_mounts(t *testing.T) { "sha": "", "running_sha": "", "version": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), }, } if diff := deep.Equal(resp.Data, exp); len(diff) > 0 { @@ -304,7 +305,7 @@ func TestSystemBackend_mount(t *testing.T) { "sha": "", "running_sha": "", "version": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), }, "sys/": map[string]interface{}{ "type": "system", @@ -324,7 +325,7 @@ func TestSystemBackend_mount(t *testing.T) { "sha": "", "running_sha": "", "version": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -343,7 +344,7 @@ func TestSystemBackend_mount(t *testing.T) { "sha": "", "running_sha": "", "version": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), }, "identity/": map[string]interface{}{ "description": "identity store", @@ -363,7 +364,7 @@ func TestSystemBackend_mount(t *testing.T) { "sha": "", "running_sha": "", "version": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), }, "prod/secret/": map[string]interface{}{ "description": "", @@ -384,7 +385,7 @@ func TestSystemBackend_mount(t *testing.T) { "sha": "", "running_sha": "", "version": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), }, } if diff := deep.Equal(resp.Data, exp); len(diff) > 0 { @@ -1928,7 +1929,7 @@ func TestSystemBackend_enableAuth(t *testing.T) { "sha": "", "running_sha": "", "version": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, }, "token/": map[string]interface{}{ "type": "token", @@ -2999,7 +3000,7 @@ func TestSystemBackend_PluginCatalog_CRUD(t *testing.T) { "args": []string(nil), "sha256": "", "builtin": true, - "version": c.pluginCatalog.getBuiltinVersion(consts.PluginTypeDatabase, "mysql-database-plugin"), + "version": versions.GetBuiltinVersion(consts.PluginTypeDatabase, "mysql-database-plugin"), "deprecation_status": deprecationStatus.String(), } if !reflect.DeepEqual(actualRespData, expectedRespData) { @@ -3329,7 +3330,7 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { "sha": "", "running_sha": "", "version": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), }, "sys/": map[string]interface{}{ "type": "system", @@ -3349,7 +3350,7 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { "sha": "", "running_sha": "", "version": "", - "running_version": "", + "running_version": versions.DefaultBuiltinVersion, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -3368,7 +3369,7 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { "sha": "", "running_sha": "", "version": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), }, "identity/": map[string]interface{}{ "description": "identity store", @@ -3388,7 +3389,7 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { "sha": "", "running_sha": "", "version": "", - "running_version": "", + "running_version": versions.GetBuiltinVersion(consts.PluginTypeSecrets, "identity"), }, }, "auth": map[string]interface{}{ diff --git a/vault/mount.go b/vault/mount.go index de60353fd..bd087b853 100644 --- a/vault/mount.go +++ b/vault/mount.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/vault/builtin/plugin" "github.com/hashicorp/vault/helper/metricsutil" "github.com/hashicorp/vault/helper/namespace" + "github.com/hashicorp/vault/helper/versions" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/jsonutil" "github.com/hashicorp/vault/sdk/logical" @@ -624,6 +625,17 @@ func (c *Core) mountInternal(ctx context.Context, entry *MountEntry, updateStora } } + // update the entry running version with the backend's reported version + if versioner, ok := backend.(logical.PluginVersioner); ok { + entry.RunningVersion = versioner.PluginVersion().Version + } + if entry.RunningVersion == "" { + // don't set the running version to a builtin if it is running as an external plugin + if externaler, ok := backend.(logical.Externaler); !ok || !externaler.IsExternal() { + entry.RunningVersion = versions.GetBuiltinVersion(consts.PluginTypeSecrets, entry.Type) + } + } + addPathCheckers(c, entry, backend, viewPath) c.setCoreBackend(entry, backend, view) @@ -1609,6 +1621,7 @@ func (c *Core) defaultMountTable() *MountTable { Options: map[string]string{ "version": "2", }, + RunningVersion: versions.GetBuiltinVersion(consts.PluginTypeSecrets, "kv"), } table.Entries = append(table.Entries, kvMount) } @@ -1643,6 +1656,7 @@ func (c *Core) requiredMountTable() *MountTable { Accessor: cubbyholeAccessor, Local: true, BackendAwareUUID: cubbyholeBackendUUID, + RunningVersion: versions.GetBuiltinVersion(consts.PluginTypeSecrets, "cubbyhole"), } sysUUID, err := uuid.GenerateUUID() @@ -1669,6 +1683,7 @@ func (c *Core) requiredMountTable() *MountTable { Config: MountConfig{ PassthroughRequestHeaders: []string{"Accept"}, }, + RunningVersion: versions.DefaultBuiltinVersion, } identityUUID, err := uuid.GenerateUUID() @@ -1694,6 +1709,7 @@ func (c *Core) requiredMountTable() *MountTable { Config: MountConfig{ PassthroughRequestHeaders: []string{"Authorization"}, }, + RunningVersion: versions.DefaultBuiltinVersion, } table.Entries = append(table.Entries, cubbyholeMount) diff --git a/vault/mount_test.go b/vault/mount_test.go index 523d06406..7a033ad45 100644 --- a/vault/mount_test.go +++ b/vault/mount_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - metrics "github.com/armon/go-metrics" + "github.com/armon/go-metrics" "github.com/go-test/deep" "github.com/hashicorp/vault/audit" "github.com/hashicorp/vault/helper/metricsutil" @@ -187,6 +187,30 @@ func TestCore_Mount(t *testing.T) { } } +func TestCore_Mount_secrets_builtin_RunningVersion(t *testing.T) { + c, _, _ := TestCoreUnsealed(t) + me := &MountEntry{ + Table: mountTableType, + Path: "foo", + Type: "generic", + } + err := c.mount(namespace.RootContext(nil), me) + if err != nil { + t.Fatalf("err: %v", err) + } + + match := c.router.MatchingMount(namespace.RootContext(nil), "foo/bar") + if match != "foo/" { + t.Fatalf("missing mount") + } + + raw, _ := c.router.root.Get(match) + // we override the running version of builtins + if !strings.Contains(raw.(*routeEntry).mountEntry.RunningVersion, "builtin") { + t.Errorf("Expected mount to have builtin version but got %s", raw.(*routeEntry).mountEntry.RunningVersion) + } +} + // TestCore_Mount_kv_generic tests that we can successfully mount kv using the // kv alias "generic" func TestCore_Mount_kv_generic(t *testing.T) { diff --git a/vault/plugin_catalog.go b/vault/plugin_catalog.go index 47c897652..95cb50421 100644 --- a/vault/plugin_catalog.go +++ b/vault/plugin_catalog.go @@ -8,15 +8,15 @@ import ( "fmt" "path" "path/filepath" - "runtime/debug" "strings" "sync" log "github.com/hashicorp/go-hclog" - multierror "github.com/hashicorp/go-multierror" - plugin "github.com/hashicorp/go-plugin" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-plugin" "github.com/hashicorp/go-secure-stdlib/base62" semver "github.com/hashicorp/go-version" + "github.com/hashicorp/vault/helper/versions" v4 "github.com/hashicorp/vault/sdk/database/dbplugin" v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "github.com/hashicorp/vault/sdk/helper/consts" @@ -24,7 +24,6 @@ import ( "github.com/hashicorp/vault/sdk/helper/pluginutil" "github.com/hashicorp/vault/sdk/logical" backendplugin "github.com/hashicorp/vault/sdk/plugin" - "github.com/hashicorp/vault/sdk/version" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) @@ -55,10 +54,6 @@ type PluginCatalog struct { externalPlugins map[string]*externalPlugin mlockPlugins bool - // once is used to ensure we only parse build info once. - once sync.Once - buildInfo *debug.BuildInfo - lock sync.RWMutex } @@ -630,7 +625,7 @@ func (c *PluginCatalog) get(ctx context.Context, name string, pluginType consts. Type: pluginType, Builtin: true, BuiltinFactory: factory, - Version: c.getBuiltinVersion(pluginType, name), + Version: versions.GetBuiltinVersion(pluginType, name), }, nil } } @@ -851,7 +846,7 @@ func (c *PluginCatalog) listInternal(ctx context.Context, pluginType consts.Plug continue } - version := c.getBuiltinVersion(pluginType, plugin) + version := versions.GetBuiltinVersion(pluginType, plugin) semanticVersion, err := semver.NewVersion(version) deprecationStatus, _ := c.builtinRegistry.DeprecationStatus(plugin, pluginType) if err != nil { @@ -874,42 +869,3 @@ func isPluginType(s string) bool { _, err := consts.ParsePluginType(s) return err == nil } - -func (c *PluginCatalog) getBuiltinVersion(pluginType consts.PluginType, pluginName string) string { - defaultBuiltinVersion := "v" + version.GetVersion().Version + "+builtin.vault" - - c.once.Do(func() { - c.buildInfo, _ = debug.ReadBuildInfo() - }) - - // Should never happen, means the binary was built without Go modules. - // Fall back to just the Vault version. - if c.buildInfo == nil { - return defaultBuiltinVersion - } - - // Vault builtin plugins are all either: - // a) An external repo within the hashicorp org - return external repo version with +builtin - // b) Within the Vault repo itself - return Vault version with +builtin.vault - // - // The repo names are predictable, but follow slightly different patterns - // for each plugin type. - t := pluginType.String() - switch pluginType { - case consts.PluginTypeDatabase: - // Database plugin built-ins are registered as e.g. "postgresql-database-plugin" - pluginName = strings.TrimSuffix(pluginName, "-database-plugin") - case consts.PluginTypeSecrets: - // Repos use "secrets", pluginType.String() is "secret". - t = "secrets" - } - pluginModulePath := fmt.Sprintf("github.com/hashicorp/vault-plugin-%s-%s", t, pluginName) - - for _, dep := range c.buildInfo.Deps { - if dep.Path == pluginModulePath { - return dep.Version + "+builtin" - } - } - - return defaultBuiltinVersion -} diff --git a/vault/plugin_catalog_test.go b/vault/plugin_catalog_test.go index f3332a70e..67999d65a 100644 --- a/vault/plugin_catalog_test.go +++ b/vault/plugin_catalog_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/builtin/credential/userpass" + "github.com/hashicorp/vault/helper/versions" "github.com/hashicorp/vault/plugins/database/postgresql" v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "github.com/hashicorp/vault/sdk/helper/consts" @@ -41,7 +42,7 @@ func TestPluginCatalog_CRUD(t *testing.T) { Name: pluginName, Type: consts.PluginTypeDatabase, Builtin: true, - Version: core.pluginCatalog.getBuiltinVersion(consts.PluginTypeDatabase, pluginName), + Version: versions.GetBuiltinVersion(consts.PluginTypeDatabase, pluginName), } expectedBuiltin.BuiltinFactory, _ = builtinplugins.Registry.Get(pluginName, consts.PluginTypeDatabase) @@ -104,7 +105,7 @@ func TestPluginCatalog_CRUD(t *testing.T) { Name: pluginName, Type: consts.PluginTypeDatabase, Builtin: true, - Version: core.pluginCatalog.getBuiltinVersion(consts.PluginTypeDatabase, pluginName), + Version: versions.GetBuiltinVersion(consts.PluginTypeDatabase, pluginName), } expectedBuiltin.BuiltinFactory, _ = builtinplugins.Registry.Get(pluginName, consts.PluginTypeDatabase)