mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-03 20:40:45 -05:00
Vault 34905 support register ce plugin with extracted artifact (#30673)
* apply oss changes from https://github.com/hashicorp/vault-enterprise/pull/8071 * handle oss file deletions * go mod tidy * add changelog
This commit is contained in:
parent
403720c1fd
commit
71edba2ccb
26 changed files with 1136 additions and 203 deletions
3
changelog/30673.txt
Normal file
3
changelog/30673.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
plugins: Support registration of CE plugins with extracted artifact directory.
|
||||
```
|
||||
|
|
@ -17,6 +17,12 @@ var (
|
|||
_ cli.CommandAutocomplete = (*PluginRegisterCommand)(nil)
|
||||
)
|
||||
|
||||
func NewPluginRegisterCommand(baseCommand *BaseCommand) cli.Command {
|
||||
return &PluginRegisterCommand{
|
||||
BaseCommand: baseCommand,
|
||||
}
|
||||
}
|
||||
|
||||
type PluginRegisterCommand struct {
|
||||
*BaseCommand
|
||||
|
||||
|
|
@ -144,8 +150,8 @@ func (c *PluginRegisterCommand) Run(args []string) int {
|
|||
case len(args) > 2:
|
||||
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1 or 2, got %d)", len(args)))
|
||||
return 1
|
||||
case c.flagSHA256 == "":
|
||||
c.UI.Error("SHA256 is required for all plugins, please provide -sha256")
|
||||
case c.flagSHA256 == "" && c.flagVersion == "":
|
||||
c.UI.Error("One of -sha256 or -version is required. If registering with binary, please provide at least -sha256 (-version optional). If registering with extracted artifact directory, please provide -version only.")
|
||||
return 1
|
||||
|
||||
// These cases should come after invalid cases have been checked
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !enterprise
|
||||
|
||||
package command
|
||||
|
||||
import "github.com/hashicorp/cli"
|
||||
|
||||
func NewPluginRegisterCommand(baseCommand *BaseCommand) cli.Command {
|
||||
return &PluginRegisterCommand{
|
||||
BaseCommand: baseCommand,
|
||||
}
|
||||
}
|
||||
5
go.mod
5
go.mod
|
|
@ -39,7 +39,8 @@ require (
|
|||
github.com/Azure/azure-storage-blob-go v0.15.0
|
||||
github.com/Azure/go-autorest/autorest v0.11.29
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.23
|
||||
github.com/ProtonMail/go-crypto v1.1.5
|
||||
github.com/ProtonMail/go-crypto v1.2.0
|
||||
github.com/ProtonMail/gopenpgp/v3 v3.2.1
|
||||
github.com/SAP/go-hdb v1.10.1
|
||||
github.com/Sectorbob/mlab-ns2 v0.0.0-20171030222938-d3aa0c295a8a
|
||||
github.com/aerospike/aerospike-client-go/v5 v5.6.0
|
||||
|
|
@ -408,7 +409,7 @@ require (
|
|||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/gophercloud/gophercloud v0.1.0 // indirect
|
||||
|
|
|
|||
6
go.sum
6
go.sum
|
|
@ -753,8 +753,10 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0
|
|||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
|
||||
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs=
|
||||
github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/ProtonMail/gopenpgp/v3 v3.2.1 h1:ohRlKL5YwyIkN5kk7uBvijiMsyA57mK0yBEJg9xButU=
|
||||
github.com/ProtonMail/gopenpgp/v3 v3.2.1/go.mod h1:x7RduTo/0n/2PjTFRoEHApaxye/8PFbhoCquwfYBUGM=
|
||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/SAP/go-hdb v1.10.1 h1:c9dGT5xHZNDwPL3NQcRpnNISn3MchwYaGoMZpCAllUs=
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/hashicorp/vault/sdk/database/dbplugin/v5/proto"
|
||||
"github.com/hashicorp/vault/sdk/helper/consts"
|
||||
"github.com/hashicorp/vault/sdk/helper/pluginutil"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
|
@ -29,6 +30,9 @@ type gRPCClient struct {
|
|||
client proto.DatabaseClient
|
||||
versionClient logical.PluginVersionClient
|
||||
doneCtx context.Context
|
||||
|
||||
// tier is the plugin tier
|
||||
tier consts.PluginTier
|
||||
}
|
||||
|
||||
func (c gRPCClient) PluginVersion() logical.PluginVersion {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ func NewPluginClient(ctx context.Context, sys pluginutil.RunnerUtil, config plug
|
|||
// order to enable multiplexing on multiplexed plugins
|
||||
c.client = proto.NewDatabaseClient(pluginClient.Conn())
|
||||
c.versionClient = logical.NewPluginVersionClient(pluginClient.Conn())
|
||||
c.tier = config.Tier
|
||||
|
||||
db = c
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ func PluginFactoryVersion(ctx context.Context, pluginName string, pluginVersion
|
|||
IsMetadataMode: false,
|
||||
AutoMTLS: true,
|
||||
Wrapper: sys,
|
||||
Tier: pluginRunner.Tier,
|
||||
}
|
||||
config.EntUpdate(pluginRunner)
|
||||
|
||||
// create a DatabasePluginClient instance
|
||||
db, err = NewPluginClient(ctx, sys, config)
|
||||
|
|
|
|||
102
sdk/helper/consts/plugin_tiers.go
Normal file
102
sdk/helper/consts/plugin_tiers.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package consts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var PluginTiers = []PluginTier{
|
||||
PluginTierUnknown,
|
||||
PluginTierCommunity,
|
||||
PluginTierPartner,
|
||||
PluginTierOfficial,
|
||||
}
|
||||
|
||||
type PluginTier uint32
|
||||
|
||||
const (
|
||||
// PluginTierUnknown defines unknown plugin tier
|
||||
// DO NOT change the order of the enum as it
|
||||
// could cause the wrong plugin tier to be read
|
||||
// from storage for a given underlying number
|
||||
PluginTierUnknown PluginTier = iota
|
||||
// PluginTierCommunity defines community plugin tier
|
||||
// DO NOT change the order of the enum as it
|
||||
// could cause the wrong plugin tier to be read
|
||||
// from storage for a given underlying number
|
||||
PluginTierCommunity
|
||||
// PluginTierPartner defines partner plugin tier
|
||||
// DO NOT change the order of the enum as it
|
||||
// could cause the wrong plugin tier to be read
|
||||
// from storage for a given underlying number
|
||||
PluginTierPartner
|
||||
// PluginTierOfficial defines enterprise plugin tier
|
||||
// DO NOT change the order of the enum as it
|
||||
// could cause the wrong plugin tier to be read
|
||||
// from storage for a given underlying number
|
||||
PluginTierOfficial
|
||||
)
|
||||
|
||||
func (p PluginTier) String() string {
|
||||
switch p {
|
||||
case PluginTierUnknown:
|
||||
return "unknown"
|
||||
case PluginTierCommunity:
|
||||
return "community"
|
||||
case PluginTierPartner:
|
||||
return "partner"
|
||||
case PluginTierOfficial:
|
||||
return "official"
|
||||
default:
|
||||
return "unsupported"
|
||||
}
|
||||
}
|
||||
|
||||
func ParsePluginTier(pluginTier string) (PluginTier, error) {
|
||||
switch pluginTier {
|
||||
case "unknown", "":
|
||||
return PluginTierUnknown, nil
|
||||
case "community":
|
||||
return PluginTierCommunity, nil
|
||||
case "partner":
|
||||
return PluginTierPartner, nil
|
||||
case "official":
|
||||
return PluginTierOfficial, nil
|
||||
default:
|
||||
return PluginTierUnknown, fmt.Errorf("%q is not a supported plugin tier", pluginTier)
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler. It supports unmarshaling either a
|
||||
// string or a uint32. All new serialization will be as a string, but we
|
||||
// previously serialized as a uint32 so we need to support that for backwards
|
||||
// compatibility.
|
||||
func (p *PluginTier) UnmarshalJSON(data []byte) error {
|
||||
var asString string
|
||||
err := json.Unmarshal(data, &asString)
|
||||
if err == nil {
|
||||
*p, err = ParsePluginTier(asString)
|
||||
return err
|
||||
}
|
||||
|
||||
var asUint32 uint32
|
||||
err = json.Unmarshal(data, &asUint32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*p = PluginTier(asUint32)
|
||||
switch *p {
|
||||
case PluginTierUnknown, PluginTierCommunity, PluginTierPartner, PluginTierOfficial:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("%d is not a supported plugin tier", asUint32)
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (p PluginTier) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(p.String())
|
||||
}
|
||||
230
sdk/helper/consts/plugin_tiers_test.go
Normal file
230
sdk/helper/consts/plugin_tiers_test.go
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package consts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParsePluginTier(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pluginTier string
|
||||
want PluginTier
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "unknown",
|
||||
pluginTier: "unknown",
|
||||
want: PluginTierUnknown,
|
||||
},
|
||||
{
|
||||
name: "empty unknown",
|
||||
pluginTier: "",
|
||||
want: PluginTierUnknown,
|
||||
},
|
||||
{
|
||||
name: "community",
|
||||
pluginTier: "community",
|
||||
want: PluginTierCommunity,
|
||||
},
|
||||
{
|
||||
name: "partner",
|
||||
pluginTier: "partner",
|
||||
want: PluginTierPartner,
|
||||
},
|
||||
{
|
||||
name: "official",
|
||||
pluginTier: "official",
|
||||
want: PluginTierOfficial,
|
||||
},
|
||||
{
|
||||
name: "unsupported",
|
||||
pluginTier: "foo",
|
||||
want: PluginTierUnknown,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParsePluginTier(tt.pluginTier)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("ParsePluginTier() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("ParsePluginTier() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginTier_MarshalJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
p PluginTier
|
||||
want []byte
|
||||
}{
|
||||
{
|
||||
name: "unknown",
|
||||
p: PluginTierUnknown,
|
||||
want: []byte(`"unknown"`),
|
||||
},
|
||||
{
|
||||
name: "community",
|
||||
p: PluginTierCommunity,
|
||||
want: []byte(`"community"`),
|
||||
},
|
||||
{
|
||||
name: "partner",
|
||||
p: PluginTierPartner,
|
||||
want: []byte(`"partner"`),
|
||||
},
|
||||
{
|
||||
name: "offical",
|
||||
p: PluginTierOfficial,
|
||||
want: []byte(`"official"`),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.p.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalJSON() error = %v, want nil", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("MarshalJSON() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginTier_UnmarshalJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
wantTier PluginTier
|
||||
data []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "unknown",
|
||||
wantTier: PluginTierUnknown,
|
||||
data: []byte(`"unknown"`),
|
||||
},
|
||||
{
|
||||
name: "community",
|
||||
wantTier: PluginTierCommunity,
|
||||
data: []byte(`"community"`),
|
||||
},
|
||||
{
|
||||
name: "partner",
|
||||
wantTier: PluginTierPartner,
|
||||
data: []byte(`"partner"`),
|
||||
},
|
||||
{
|
||||
name: "offical",
|
||||
wantTier: PluginTierOfficial,
|
||||
data: []byte(`"official"`),
|
||||
},
|
||||
{
|
||||
name: "unsupported",
|
||||
wantTier: PluginTierUnknown,
|
||||
data: []byte(`"foo"`),
|
||||
wantErr: true,
|
||||
},
|
||||
// The following test cases ensures new plugin tiers are added at the end of the enum list
|
||||
// Inserting a new plugin tier in the middle of the enum list will fail
|
||||
// New plugin tiers should be added at the end of the test case list
|
||||
{
|
||||
name: "0-unknown",
|
||||
wantTier: PluginTierUnknown,
|
||||
data: []byte(`0`),
|
||||
},
|
||||
{
|
||||
name: "1-community",
|
||||
wantTier: PluginTierCommunity,
|
||||
data: []byte(`1`),
|
||||
},
|
||||
{
|
||||
name: "2-partner",
|
||||
wantTier: PluginTierPartner,
|
||||
data: []byte(`2`),
|
||||
},
|
||||
{
|
||||
name: "3-official",
|
||||
wantTier: PluginTierOfficial,
|
||||
data: []byte(`3`),
|
||||
},
|
||||
{
|
||||
name: "tier number unsupported",
|
||||
data: []byte(`2345678`),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var tier PluginTier
|
||||
err := tier.UnmarshalJSON(tt.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if !tt.wantErr && tier != tt.wantTier {
|
||||
t.Fatalf("UnmarshalJSON() got = %v, want %v", tier, tt.wantTier)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPluginTierJSONRoundTrip tests that PluginTier can be marshaled and unmarshaled
|
||||
// to/from JSON in a round trip.
|
||||
func TestPluginTierJSONRoundTrip(t *testing.T) {
|
||||
type testTier struct {
|
||||
PluginTier PluginTier `json:"plugin_tier"`
|
||||
}
|
||||
|
||||
for _, tier := range PluginTiers {
|
||||
t.Run(tier.String(), func(t *testing.T) {
|
||||
original := testTier{
|
||||
PluginTier: tier,
|
||||
}
|
||||
asBytes, err := json.Marshal(original)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var roundTripped testTier
|
||||
err = json.Unmarshal(asBytes, &roundTripped)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if original != roundTripped {
|
||||
t.Fatalf("expected %v, got %v", original, roundTripped)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownTierExcludedWithOmitEmpty(t *testing.T) {
|
||||
type testTierOmitEmpty struct {
|
||||
Type PluginTier `json:"tier,omitempty"`
|
||||
}
|
||||
bytes, err := json.Marshal(testTierOmitEmpty{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := map[string]any{}
|
||||
json.Unmarshal(bytes, &m)
|
||||
if _, exists := m["tier"]; exists {
|
||||
t.Fatal("tier should not be present")
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,6 @@ const (
|
|||
)
|
||||
|
||||
type PluginClientConfig struct {
|
||||
EntPluginClientConfig
|
||||
Name string
|
||||
PluginType consts.PluginType
|
||||
Version string
|
||||
|
|
@ -41,6 +40,7 @@ type PluginClientConfig struct {
|
|||
AutoMTLS bool
|
||||
MLock bool
|
||||
Wrapper RunnerUtil
|
||||
Tier consts.PluginTier
|
||||
}
|
||||
|
||||
type runConfig struct {
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build !enterprise
|
||||
|
||||
package pluginutil
|
||||
|
||||
type EntPluginClientConfig struct{}
|
||||
|
||||
func (p *PluginClientConfig) EntUpdate(_ *PluginRunner) {
|
||||
// no-op
|
||||
}
|
||||
|
|
@ -18,6 +18,13 @@ import (
|
|||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
// ConfigPluginTier is the key for the plugin tier for Config of logical.BackendConfig
|
||||
ConfigPluginTier = "plugin_tier"
|
||||
// ConfigPluginVersion is the key for the plugin version for Config of logical.BackendConfig
|
||||
ConfigPluginVersion = "plugin_version"
|
||||
)
|
||||
|
||||
// ErrPluginNotFound is returned when a plugin does not have a pinned version.
|
||||
var ErrPinnedVersionNotFound = errors.New("pinned version not found")
|
||||
|
||||
|
|
@ -57,8 +64,6 @@ const MultiplexingCtxKey string = "multiplex_id"
|
|||
// PluginRunner defines the metadata needed to run a plugin securely with
|
||||
// go-plugin.
|
||||
type PluginRunner struct {
|
||||
EntPluginRunner
|
||||
|
||||
Name string `json:"name" structs:"name"`
|
||||
Type consts.PluginType `json:"type" structs:"type"`
|
||||
Version string `json:"version" structs:"version"`
|
||||
|
|
@ -69,6 +74,7 @@ type PluginRunner struct {
|
|||
Env []string `json:"env" structs:"env"`
|
||||
Sha256 []byte `json:"sha256" structs:"sha256"`
|
||||
Builtin bool `json:"builtin" structs:"builtin"`
|
||||
Tier consts.PluginTier `json:"tier" structs:"tier"`
|
||||
BuiltinFactory func() (interface{}, error) `json:"-" structs:"-"`
|
||||
RuntimeConfig *prutil.PluginRuntimeConfig `json:"-" structs:"-"`
|
||||
Tmpdir string `json:"-" structs:"-"`
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build !enterprise
|
||||
|
||||
package pluginutil
|
||||
|
||||
type EntPluginRunner struct{}
|
||||
|
|
@ -987,7 +987,7 @@ func (c *Core) newCredentialBackend(ctx context.Context, entry *MountEntry, sysV
|
|||
factory = wrapFactoryCheckPerms(c, plugin.Factory)
|
||||
}
|
||||
|
||||
entSetExternalPluginConfig(plug, conf)
|
||||
setExternalPluginConfig(plug, conf)
|
||||
}
|
||||
|
||||
// Set up conf to pass in plugin_name
|
||||
|
|
|
|||
|
|
@ -541,9 +541,10 @@ func (b *SystemBackend) handlePluginCatalogUpdate(ctx context.Context, _ *logica
|
|||
sha256 := d.Get("sha256").(string)
|
||||
if sha256 == "" {
|
||||
sha256 = d.Get("sha_256").(string)
|
||||
if resp := validateSHA256(sha256); resp.IsError() {
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
if sha256 == "" && pluginVersion == "" {
|
||||
return logical.ErrorResponse("must provide at least one of sha256 or version: use sha256 for binary registration (version optional) or version only for artifact registration"), nil
|
||||
}
|
||||
|
||||
if resp := validateSha256IsEmptyForEntPluginVersion(pluginVersion, sha256); resp.IsError() {
|
||||
|
|
|
|||
|
|
@ -9,13 +9,6 @@ import (
|
|||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
func validateSHA256(sha256 string) *logical.Response {
|
||||
if sha256 == "" {
|
||||
return logical.ErrorResponse("missing SHA-256 value")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSha256IsEmptyForEntPluginVersion(pluginVersion string, sha256 string) *logical.Response {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1717,7 +1717,7 @@ func (c *Core) newLogicalBackend(ctx context.Context, entry *MountEntry, sysView
|
|||
factory = wrapFactoryCheckPerms(c, factory)
|
||||
}
|
||||
|
||||
entSetExternalPluginConfig(plug, conf)
|
||||
setExternalPluginConfig(plug, conf)
|
||||
}
|
||||
|
||||
// Set up conf to pass in plugin_name
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import (
|
|||
"path"
|
||||
|
||||
"github.com/hashicorp/vault/helper/namespace"
|
||||
"github.com/hashicorp/vault/sdk/helper/pluginutil"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
|
|
@ -77,8 +76,3 @@ func (c *Core) mountEntrySysView(entry *MountEntry) extendedSystemView {
|
|||
func (c *Core) entBuiltinPluginMetrics(ctx context.Context, entry *MountEntry, val float32) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// entSetExternalPluginConfig (Vault Community edition) makes no changes to config for external plugins.
|
||||
func entSetExternalPluginConfig(_ *pluginutil.PluginRunner, _ map[string]string) {
|
||||
// No-op
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
package vault
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/vault/sdk/helper/pluginutil"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
|
|
@ -29,3 +30,8 @@ func collectBackendSpecialPaths(backend logical.Backend, viewPath string, access
|
|||
|
||||
return ret
|
||||
}
|
||||
|
||||
// setExternalPluginConfig sets key value pairs to config based on pluginutil.PluginRunner
|
||||
func setExternalPluginConfig(runner *pluginutil.PluginRunner, config map[string]string) {
|
||||
config[pluginutil.ConfigPluginTier] = runner.Tier.String()
|
||||
}
|
||||
|
|
|
|||
252
vault/plugincatalog/plugin_artifact.go
Normal file
252
vault/plugincatalog/plugin_artifact.go
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package plugincatalog
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v3/crypto"
|
||||
)
|
||||
|
||||
// hashiCorpPGPPubKey is HashiCorp's PGP public key at https://www.hashicorp.com/.well-known/pgp-key.txt.
|
||||
// This key is used to verify the authenticity of HashiCorp plugins.
|
||||
const hashiCorpPGPPubKey = `
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
ZF5q4h4I33PSGDdSvGXn9UMY5Isjpg==
|
||||
=7pIB
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
`
|
||||
|
||||
var defaultPGPPubKey = hashiCorpPGPPubKey
|
||||
|
||||
const (
|
||||
extractedArtifactDirFmt = "%s_%s_%s_%s" // vault-plugin-database-oracle_1.2.3+ent_linux_amd64
|
||||
metadataFile = "metadata.json"
|
||||
metadataSig = "metadata.json.sig"
|
||||
)
|
||||
|
||||
var (
|
||||
errExtractedArtifactDirNotFound = errors.New("extracted artifact directory not found")
|
||||
errReadMetadata = errors.New("failed to read metadata")
|
||||
errReadMetadataSig = errors.New("failed to read metadata signature")
|
||||
errReadPlugin = errors.New("failed to read plugin binary")
|
||||
errReadPluginMetadata = errors.New("failed to read plugin metadata")
|
||||
errReadPluginPGPSig = errors.New("failed to read plugin binary PGP signature")
|
||||
errVerifyMetadataSig = errors.New("failed to verify metadata detached signature")
|
||||
errVerifyPluginSig = errors.New("failed to verify plugin binary PGP signature")
|
||||
)
|
||||
|
||||
func getExtractedArtifactDir(pluginName, pluginVersion string) string {
|
||||
if strings.HasPrefix(pluginVersion, "v") {
|
||||
pluginVersion = pluginVersion[1:]
|
||||
}
|
||||
|
||||
return fmt.Sprintf(extractedArtifactDirFmt, pluginName, pluginVersion, runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
func verifyPlugin(pluginDir, pluginName, pubKey string) (*pluginMetadata, error) {
|
||||
// verify the extracted plugin artifact directory structure and each file inside
|
||||
// vault-plugin-secrets-example_1.2.3+ent_darwin_arm64/EULA.txt (optional)
|
||||
// vault-plugin-secrets-example_1.2.3+ent_darwin_arm64/TermsOfEvaluation.txt (optional)
|
||||
// vault-plugin-secrets-example_1.2.3+ent_darwin_arm64/LICENSE (optional)
|
||||
// vault-plugin-secrets-example_1.2.3+ent_darwin_arm64/vault-plugin-secrets-example
|
||||
// vault-plugin-secrets-example_1.2.3+ent_darwin_arm64/metadata.json
|
||||
// vault-plugin-secrets-example_1.2.3+ent_darwin_arm64/metadata.json.sig
|
||||
|
||||
if _, err := os.Stat(pluginDir); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("%w: %w", errExtractedArtifactDirNotFound, err)
|
||||
}
|
||||
|
||||
pgp := crypto.PGP()
|
||||
key, err := crypto.NewKeyFromArmored(pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
verifier, err := pgp.Verify().VerificationKey(key).New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// verify metadata.json is untampered
|
||||
metadataPath := path.Join(pluginDir, "metadata.json")
|
||||
metadataBytes, err := os.ReadFile(metadataPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", errReadMetadata, err)
|
||||
}
|
||||
|
||||
metadataSigBytes, err := os.ReadFile(path.Join(pluginDir, "metadata.json.sig"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", errReadMetadataSig, err)
|
||||
}
|
||||
|
||||
verifyResult, err := verifier.VerifyDetached(metadataBytes, metadataSigBytes, crypto.Armor)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", errVerifyMetadataSig, err)
|
||||
}
|
||||
if sigErr := verifyResult.SignatureError(); sigErr != nil {
|
||||
return nil, fmt.Errorf("%w: %w", errVerifyMetadataSig, sigErr)
|
||||
}
|
||||
|
||||
// verify plugin binary is untampred
|
||||
pluginBytes, err := os.ReadFile(path.Join(pluginDir, pluginName))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", errReadPlugin, err)
|
||||
}
|
||||
|
||||
metadata, err := readPluginMetadata(metadataPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", errReadPluginMetadata, err)
|
||||
}
|
||||
|
||||
verifyResult, err = verifier.VerifyDetached(pluginBytes, []byte(metadata.Plugin.PGPSig), crypto.Armor)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", errVerifyPluginSig, err)
|
||||
}
|
||||
if sigErr := verifyResult.SignatureError(); sigErr != nil {
|
||||
return nil, fmt.Errorf("%w: %w", errVerifyPluginSig, sigErr)
|
||||
}
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func pluginSHA256Sum(path string) ([]byte, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hash := sha256.New()
|
||||
if _, err = io.Copy(hash, file); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return hash.Sum(nil), nil
|
||||
}
|
||||
190
vault/plugincatalog/plugin_artifact_test.go
Normal file
190
vault/plugincatalog/plugin_artifact_test.go
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package plugincatalog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v3/crypto"
|
||||
"github.com/hashicorp/vault/sdk/helper/consts"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test_verifyPlugin tests the verifyPlugin function.
|
||||
func Test_verifyPlugin(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type args struct {
|
||||
pluginName string
|
||||
pluginVersion string
|
||||
pluginType consts.PluginType
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
pluginName: "vault-plugin-auth-example",
|
||||
pluginVersion: "0.1.1+ent",
|
||||
pluginType: consts.PluginTypeCredential,
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "missing metadata",
|
||||
args: args{
|
||||
pluginName: "vault-plugin-auth-example",
|
||||
pluginVersion: "0.1.2+ent",
|
||||
pluginType: consts.PluginTypeCredential,
|
||||
},
|
||||
expectedErr: errReadMetadata,
|
||||
},
|
||||
{
|
||||
name: "missing metadata signature",
|
||||
args: args{
|
||||
pluginName: "vault-plugin-auth-example",
|
||||
pluginVersion: "0.1.3+ent",
|
||||
pluginType: consts.PluginTypeCredential,
|
||||
},
|
||||
expectedErr: errReadMetadataSig,
|
||||
},
|
||||
{
|
||||
name: "bad metadata signature verify",
|
||||
args: args{
|
||||
pluginName: "vault-plugin-secret-example",
|
||||
pluginVersion: "0.1.4+ent",
|
||||
pluginType: consts.PluginTypeSecrets,
|
||||
},
|
||||
expectedErr: errVerifyMetadataSig,
|
||||
},
|
||||
{
|
||||
name: "missing plugin binary",
|
||||
args: args{
|
||||
pluginName: "vault-plugin-database-example",
|
||||
pluginVersion: "0.1.5+ent",
|
||||
pluginType: consts.PluginTypeDatabase,
|
||||
},
|
||||
expectedErr: errReadPlugin,
|
||||
},
|
||||
{
|
||||
name: "bad plugin binary signature verify",
|
||||
args: args{
|
||||
pluginName: "vault-plugin-database-example",
|
||||
pluginVersion: "0.1.6+ent",
|
||||
pluginType: consts.PluginTypeDatabase,
|
||||
},
|
||||
expectedErr: errVerifyPluginSig,
|
||||
},
|
||||
{
|
||||
name: "bad extracted artifact directory",
|
||||
args: args{
|
||||
pluginName: "vault-plugin-database-example",
|
||||
pluginVersion: "0.1.6+ent",
|
||||
pluginType: consts.PluginTypeDatabase,
|
||||
},
|
||||
expectedErr: errExtractedArtifactDirNotFound,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
privKey, pubKeyArmored := generatePGPKeyPair(t)
|
||||
|
||||
contents := generatePluginArtifactContents(t, tt.args.pluginName,
|
||||
tt.args.pluginVersion, tt.args.pluginType, !errors.Is(tt.expectedErr, errReadPluginPGPSig), privKey)
|
||||
|
||||
actualExtractedArtifactDir := getExtractedArtifactDir(tt.args.pluginName, tt.args.pluginVersion)
|
||||
switch {
|
||||
case tt.expectedErr == nil:
|
||||
case errors.Is(tt.expectedErr, errReadPluginPGPSig):
|
||||
// no-op
|
||||
case errors.Is(tt.expectedErr, errReadMetadata):
|
||||
delete(contents, metadataFile)
|
||||
case errors.Is(tt.expectedErr, errReadMetadataSig):
|
||||
delete(contents, metadataSig)
|
||||
case errors.Is(tt.expectedErr, errVerifyMetadataSig):
|
||||
contents[metadataFile] = []byte(`{"will not" : "match signature"}`)
|
||||
case errors.Is(tt.expectedErr, errReadPlugin):
|
||||
delete(contents, tt.args.pluginName)
|
||||
case errors.Is(tt.expectedErr, errVerifyPluginSig):
|
||||
contents[tt.args.pluginName] = []byte("will not match signature")
|
||||
case errors.Is(tt.expectedErr, errExtractedArtifactDirNotFound):
|
||||
actualExtractedArtifactDir += "not_found"
|
||||
default:
|
||||
t.Fatalf("unexpected error: %v", tt.expectedErr)
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
actualPluginDir := filepath.Join(tempDir, actualExtractedArtifactDir)
|
||||
err := os.MkdirAll(actualPluginDir, 0o755)
|
||||
assert.NoError(t, err, "expected successful create extracted plugin directory")
|
||||
|
||||
// Write the files to the extracted plugin directory
|
||||
for name, content := range contents {
|
||||
err = os.WriteFile(filepath.Join(actualPluginDir, name), content, 0o644)
|
||||
assert.NoError(t, err, "expected successful file write")
|
||||
}
|
||||
var metadata *pluginMetadata
|
||||
metadata, err = verifyPlugin(path.Join(tempDir, getExtractedArtifactDir(tt.args.pluginName, tt.args.pluginVersion)),
|
||||
tt.args.pluginName, pubKeyArmored)
|
||||
assert.ErrorIs(t, err, tt.expectedErr, "expected verify plugin error to match")
|
||||
|
||||
if tt.expectedErr == nil {
|
||||
assert.NotNil(t, metadata)
|
||||
assert.Equal(t, tt.args.pluginName, metadata.Plugin.Name)
|
||||
assert.Equal(t, tt.args.pluginVersion, metadata.Plugin.Version)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test_getExtractedArtifactDir tests the getExtractedArtifactDir function.
|
||||
func Test_getExtractedArtifactDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type args struct {
|
||||
command string
|
||||
version string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "v-prefixed version",
|
||||
args: args{"vault-plugin-auth-aws", "v0.18.0+ent"},
|
||||
want: fmt.Sprintf("vault-plugin-auth-aws_0.18.0+ent_%s_%s", runtime.GOOS, runtime.GOARCH),
|
||||
},
|
||||
{
|
||||
name: "un-prefixed version",
|
||||
args: args{"vault-plugin-auth-aws", "0.18.0+ent"},
|
||||
want: fmt.Sprintf("vault-plugin-auth-aws_0.18.0+ent_%s_%s", runtime.GOOS, runtime.GOARCH),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, getExtractedArtifactDir(tt.args.command, tt.args.version))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPluginCatalog_hashiCorpPubPGPKey tests hashiCorpPubPGPKey read
|
||||
// and verification key creation.
|
||||
func TestPluginCatalog_hashiCorpPubPGPKey(t *testing.T) {
|
||||
pgp := crypto.PGP()
|
||||
key, err := crypto.NewKeyFromArmored(hashiCorpPGPPubKey)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = pgp.Verify().VerificationKey(key).New()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
|
@ -174,7 +174,7 @@ func SetupPluginCatalog(ctx context.Context, in *PluginCatalogInput) (*PluginCat
|
|||
}
|
||||
|
||||
// Sanitize the plugin catalog
|
||||
err = catalog.entValidate(ctx)
|
||||
err = catalog.verifyOfficialPlugins(ctx)
|
||||
if err != nil {
|
||||
logger.Error("error while sanitizing plugin storage", "error", err)
|
||||
return nil, err
|
||||
|
|
@ -864,6 +864,33 @@ func (c *PluginCatalog) upgradePlugins(ctx context.Context, logger log.Logger) e
|
|||
return retErr
|
||||
}
|
||||
|
||||
// verifyOfficialPlugins verifies all official HashiCorp plugins
|
||||
func (c *PluginCatalog) verifyOfficialPlugins(ctx context.Context) error {
|
||||
plugins, err := c.collectAllPlugins(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var hasOfficialPlugins bool
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Tier != consts.PluginTierOfficial {
|
||||
continue
|
||||
}
|
||||
|
||||
hasOfficialPlugins = true
|
||||
pluginDir := path.Join(c.directory, path.Dir(plugin.Command))
|
||||
if _, err = verifyPlugin(pluginDir, plugin.Name, hashiCorpPGPPubKey); err != nil {
|
||||
return fmt.Errorf("failed to verify plugin %q version %q: %w", plugin.Name, plugin.Version, err)
|
||||
}
|
||||
}
|
||||
|
||||
if hasOfficialPlugins {
|
||||
c.logger.Info("all official plugins verified")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retrieves a plugin with the specified name from the catalog. It first
|
||||
// looks for external plugins with this name and then looks for builtin plugins.
|
||||
// It returns a PluginRunner or an error if no plugin was found.
|
||||
|
|
@ -966,6 +993,165 @@ func (c *PluginCatalog) Set(ctx context.Context, plugin pluginutil.SetPluginInpu
|
|||
return err
|
||||
}
|
||||
|
||||
// setInternal sets a plugin entry in the catalog. In addition to its CE functionality,
|
||||
// it will attempt to verify the plugin in its extracted artifact directory
|
||||
// if the sha256 is not specified.
|
||||
func (c *PluginCatalog) setInternal(ctx context.Context, plugin pluginutil.SetPluginInput) (*pluginutil.PluginRunner, error) {
|
||||
command := plugin.Command
|
||||
var pluginTier consts.PluginTier
|
||||
|
||||
if plugin.OCIImage == "" {
|
||||
// When OCIImage is empty, then we want to register with the binary either directly (if Sha256 is set) or via the extracted artifact directory (if Sha256 is unset).
|
||||
|
||||
var expectedPluginDir string
|
||||
if len(plugin.Sha256) > 0 {
|
||||
// When Sha256 is set, we can assume the binary is already available.
|
||||
expectedPluginDir = c.directory
|
||||
command = filepath.Join(c.directory, plugin.Command)
|
||||
} else {
|
||||
// When Sha256 is unset, ensure Version is set then attempt to verify the plugin
|
||||
// in its extracted artifact directory.
|
||||
|
||||
if len(plugin.Version) == 0 {
|
||||
return nil, fmt.Errorf("must specify sha256 to register plugin with binary or version to register plugin with extracted artifact directory")
|
||||
}
|
||||
|
||||
var err error
|
||||
extractedArtifactDir := getExtractedArtifactDir(plugin.Name, plugin.Version)
|
||||
expectedPluginDir = path.Join(c.directory, extractedArtifactDir)
|
||||
plugin.Command = path.Join(extractedArtifactDir, plugin.Name)
|
||||
|
||||
metadata, err := verifyPlugin(expectedPluginDir, plugin.Name, defaultPGPPubKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to verify plugin plugin %q version %q: %w",
|
||||
plugin.Name, plugin.Version, err)
|
||||
}
|
||||
pluginTier = metadata.Plugin.Tier
|
||||
|
||||
plugin.Sha256, err = pluginSHA256Sum(path.Join(c.directory, plugin.Command))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to calculate SHA256 of plugin: %w", err)
|
||||
}
|
||||
|
||||
command = filepath.Join(c.directory, plugin.Command)
|
||||
}
|
||||
|
||||
sym, err := filepath.EvalSymlinks(command)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while validating the command path: %w", err)
|
||||
}
|
||||
|
||||
// Best effort check to make sure the command isn't breaking out of the
|
||||
// configured plugin directory.
|
||||
|
||||
symAbs, err := filepath.Abs(filepath.Dir(sym))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while validating the command path: %w", err)
|
||||
}
|
||||
|
||||
if symAbs != expectedPluginDir {
|
||||
return nil, fmt.Errorf("cannot execute files outside of configured plugin directory %s: expected %s, got %s", c.directory, expectedPluginDir, symAbs)
|
||||
}
|
||||
}
|
||||
|
||||
// entryTmp should only be used for the below type and version checks. It uses the
|
||||
// full command instead of the relative command because get() normally prepends
|
||||
// the plugin directory to the command, but we can't use get() here.
|
||||
entryTmp := &pluginutil.PluginRunner{
|
||||
Name: plugin.Name,
|
||||
Command: command,
|
||||
OCIImage: plugin.OCIImage,
|
||||
Runtime: plugin.Runtime,
|
||||
Args: plugin.Args,
|
||||
Env: plugin.Env,
|
||||
Sha256: plugin.Sha256,
|
||||
Builtin: false,
|
||||
Tmpdir: c.tmpdir,
|
||||
Tier: pluginTier,
|
||||
}
|
||||
|
||||
if entryTmp.OCIImage != "" && entryTmp.Runtime != "" {
|
||||
var err error
|
||||
entryTmp.RuntimeConfig, err = c.runtimeCatalog.Get(ctx, entryTmp.Runtime, consts.PluginRuntimeTypeContainer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get configured runtime for plugin %q: %w", plugin.Name, err)
|
||||
}
|
||||
}
|
||||
// If the plugin type is unknown, we want to attempt to determine the type
|
||||
if plugin.Type == consts.PluginTypeUnknown {
|
||||
var err error
|
||||
plugin.Type, err = c.getPluginTypeFromUnknown(ctx, entryTmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if plugin.Type == consts.PluginTypeUnknown {
|
||||
return nil, ErrPluginBadType
|
||||
}
|
||||
}
|
||||
|
||||
// getting the plugin version is best-effort, so errors are not fatal
|
||||
runningVersion := logical.EmptyPluginVersion
|
||||
var versionErr error
|
||||
switch plugin.Type {
|
||||
case consts.PluginTypeSecrets, consts.PluginTypeCredential:
|
||||
runningVersion, versionErr = c.getBackendRunningVersion(ctx, entryTmp)
|
||||
case consts.PluginTypeDatabase:
|
||||
runningVersion, versionErr = c.getDatabaseRunningVersion(ctx, entryTmp)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown plugin type: %v", plugin.Type)
|
||||
}
|
||||
if versionErr != nil {
|
||||
c.logger.Warn("Error determining plugin version", "error", versionErr)
|
||||
if errors.Is(versionErr, ErrPluginUnableToRun) {
|
||||
return nil, versionErr
|
||||
}
|
||||
} else if plugin.Version != "" && runningVersion.Version != "" && plugin.Version != runningVersion.Version {
|
||||
c.logger.Error("Plugin self-reported version did not match requested version",
|
||||
"plugin", plugin.Name, "requestedVersion", plugin.Version, "reportedVersion", runningVersion.Version)
|
||||
return nil, fmt.Errorf("%w: %s reported version (%s) did not match requested version (%s)",
|
||||
ErrPluginVersionMismatch, plugin.Name, runningVersion.Version, plugin.Version)
|
||||
} else if plugin.Version == "" && runningVersion.Version != "" {
|
||||
plugin.Version = runningVersion.Version
|
||||
_, err := semver.NewVersion(plugin.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("plugin self-reported version %q is not a valid semantic version: %w", plugin.Version, err)
|
||||
}
|
||||
}
|
||||
|
||||
entry := &pluginutil.PluginRunner{
|
||||
Name: plugin.Name,
|
||||
Type: plugin.Type,
|
||||
Version: plugin.Version,
|
||||
Command: plugin.Command,
|
||||
OCIImage: plugin.OCIImage,
|
||||
Runtime: plugin.Runtime,
|
||||
Args: plugin.Args,
|
||||
Env: plugin.Env,
|
||||
Sha256: plugin.Sha256,
|
||||
Builtin: false,
|
||||
Tmpdir: c.tmpdir,
|
||||
Tier: pluginTier,
|
||||
}
|
||||
|
||||
buf, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode plugin entry: %w", err)
|
||||
}
|
||||
|
||||
storageKey := path.Join(plugin.Type.String(), plugin.Name)
|
||||
if plugin.Version != "" {
|
||||
storageKey = path.Join(storageKey, plugin.Version)
|
||||
}
|
||||
logicalEntry := logical.StorageEntry{
|
||||
Key: storageKey,
|
||||
Value: buf,
|
||||
}
|
||||
if err := c.catalogView.Put(ctx, &logicalEntry); err != nil {
|
||||
return nil, fmt.Errorf("failed to persist plugin entry: %w", err)
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// Delete is used to remove an external plugin from the catalog. Builtin plugins
|
||||
// can not be deleted.
|
||||
func (c *PluginCatalog) Delete(ctx context.Context, name string, pluginType consts.PluginType, pluginVersion string) error {
|
||||
|
|
|
|||
|
|
@ -1,140 +0,0 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !enterprise
|
||||
|
||||
package plugincatalog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
semver "github.com/hashicorp/go-version"
|
||||
"github.com/hashicorp/vault/sdk/helper/consts"
|
||||
"github.com/hashicorp/vault/sdk/helper/pluginutil"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
// setInternal creates a new plugin entry in the catalog and persists it to storage
|
||||
func (c *PluginCatalog) setInternal(ctx context.Context, plugin pluginutil.SetPluginInput) (*pluginutil.PluginRunner, error) {
|
||||
command := plugin.Command
|
||||
if plugin.OCIImage == "" {
|
||||
// Best effort check to make sure the command isn't breaking out of the
|
||||
// configured plugin directory.
|
||||
command = filepath.Join(c.directory, plugin.Command)
|
||||
sym, err := filepath.EvalSymlinks(command)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while validating the command path: %w", err)
|
||||
}
|
||||
symAbs, err := filepath.Abs(filepath.Dir(sym))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while validating the command path: %w", err)
|
||||
}
|
||||
|
||||
if symAbs != c.directory {
|
||||
return nil, errors.New("cannot execute files outside of configured plugin directory")
|
||||
}
|
||||
}
|
||||
|
||||
// entryTmp should only be used for the below type and version checks. It uses the
|
||||
// full command instead of the relative command because get() normally prepends
|
||||
// the plugin directory to the command, but we can't use get() here.
|
||||
entryTmp := &pluginutil.PluginRunner{
|
||||
Name: plugin.Name,
|
||||
Command: command,
|
||||
OCIImage: plugin.OCIImage,
|
||||
Runtime: plugin.Runtime,
|
||||
Args: plugin.Args,
|
||||
Env: plugin.Env,
|
||||
Sha256: plugin.Sha256,
|
||||
Builtin: false,
|
||||
Tmpdir: c.tmpdir,
|
||||
}
|
||||
if entryTmp.OCIImage != "" && entryTmp.Runtime != "" {
|
||||
var err error
|
||||
entryTmp.RuntimeConfig, err = c.runtimeCatalog.Get(ctx, entryTmp.Runtime, consts.PluginRuntimeTypeContainer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get configured runtime for plugin %q: %w", plugin.Name, err)
|
||||
}
|
||||
}
|
||||
// If the plugin type is unknown, we want to attempt to determine the type
|
||||
if plugin.Type == consts.PluginTypeUnknown {
|
||||
var err error
|
||||
plugin.Type, err = c.getPluginTypeFromUnknown(ctx, entryTmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if plugin.Type == consts.PluginTypeUnknown {
|
||||
return nil, ErrPluginBadType
|
||||
}
|
||||
}
|
||||
|
||||
// getting the plugin version is best-effort, so errors are not fatal
|
||||
runningVersion := logical.EmptyPluginVersion
|
||||
var versionErr error
|
||||
switch plugin.Type {
|
||||
case consts.PluginTypeSecrets, consts.PluginTypeCredential:
|
||||
runningVersion, versionErr = c.getBackendRunningVersion(ctx, entryTmp)
|
||||
case consts.PluginTypeDatabase:
|
||||
runningVersion, versionErr = c.getDatabaseRunningVersion(ctx, entryTmp)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown plugin type: %v", plugin.Type)
|
||||
}
|
||||
if versionErr != nil {
|
||||
c.logger.Warn("Error determining plugin version", "error", versionErr)
|
||||
if errors.Is(versionErr, ErrPluginUnableToRun) {
|
||||
return nil, versionErr
|
||||
}
|
||||
} else if plugin.Version != "" && runningVersion.Version != "" && plugin.Version != runningVersion.Version {
|
||||
c.logger.Error("Plugin self-reported version did not match requested version",
|
||||
"plugin", plugin.Name, "requestedVersion", plugin.Version, "reportedVersion", runningVersion.Version)
|
||||
return nil, fmt.Errorf("%w: %s reported version (%s) did not match requested version (%s)",
|
||||
ErrPluginVersionMismatch, plugin.Name, runningVersion.Version, plugin.Version)
|
||||
} else if plugin.Version == "" && runningVersion.Version != "" {
|
||||
plugin.Version = runningVersion.Version
|
||||
_, err := semver.NewVersion(plugin.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("plugin self-reported version %q is not a valid semantic version: %w", plugin.Version, err)
|
||||
}
|
||||
}
|
||||
|
||||
entry := &pluginutil.PluginRunner{
|
||||
Name: plugin.Name,
|
||||
Type: plugin.Type,
|
||||
Version: plugin.Version,
|
||||
Command: plugin.Command,
|
||||
OCIImage: plugin.OCIImage,
|
||||
Runtime: plugin.Runtime,
|
||||
Args: plugin.Args,
|
||||
Env: plugin.Env,
|
||||
Sha256: plugin.Sha256,
|
||||
Builtin: false,
|
||||
Tmpdir: c.tmpdir,
|
||||
}
|
||||
|
||||
buf, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode plugin entry: %w", err)
|
||||
}
|
||||
|
||||
storageKey := path.Join(plugin.Type.String(), plugin.Name)
|
||||
if plugin.Version != "" {
|
||||
storageKey = path.Join(storageKey, plugin.Version)
|
||||
}
|
||||
logicalEntry := logical.StorageEntry{
|
||||
Key: storageKey,
|
||||
Value: buf,
|
||||
}
|
||||
if err := c.catalogView.Put(ctx, &logicalEntry); err != nil {
|
||||
return nil, fmt.Errorf("failed to persist plugin entry: %w", err)
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (c *PluginCatalog) entValidate(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
43
vault/plugincatalog/plugin_metadata.go
Normal file
43
vault/plugincatalog/plugin_metadata.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package plugincatalog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/helper/consts"
|
||||
)
|
||||
|
||||
type pluginMetadata struct {
|
||||
Version string `json:"version"`
|
||||
Plugin Plugin `json:"plugin"`
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
Name string `json:"name"`
|
||||
Type consts.PluginType `json:"type"`
|
||||
Tier consts.PluginTier `json:"tier,omitempty"`
|
||||
// By is the plugin author's GitHub account, following Terraform Registry's convention
|
||||
By string `json:"by"`
|
||||
Version string `json:"version"`
|
||||
Platform string `json:"platform"`
|
||||
Arch string `json:"arch"`
|
||||
// PGPSig is PGP ASCII armored detached signature
|
||||
PGPSig string `json:"pgp_sig"`
|
||||
}
|
||||
|
||||
func readPluginMetadata(metadataPath string) (*pluginMetadata, error) {
|
||||
metadataBytes, err := os.ReadFile(metadataPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metadata := pluginMetadata{}
|
||||
if err = json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &metadata, nil
|
||||
}
|
||||
87
vault/plugincatalog/testing_util.go
Normal file
87
vault/plugincatalog/testing_util.go
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package plugincatalog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v3/crypto"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/vault/sdk/helper/consts"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// gereratePGPKeyPair generates a PGP key pair for testing purposes
|
||||
func generatePGPKeyPair(t *testing.T) (*crypto.Key, string) {
|
||||
pgp := crypto.PGP()
|
||||
user := "test" + uuid.NewString()[:5]
|
||||
privKey, err := pgp.KeyGeneration().AddUserId(user, fmt.Sprintf("%s@hashicorp.com", user)).New().GenerateKey()
|
||||
assert.NoError(t, err)
|
||||
|
||||
pubkey, err := privKey.ToPublic()
|
||||
assert.NoError(t, err)
|
||||
|
||||
armored, err := pubkey.Armor()
|
||||
assert.NoError(t, err)
|
||||
|
||||
return privKey, armored
|
||||
}
|
||||
|
||||
// generatePluginArtifactContents generates file contents for a plugin artifact for testing purposes
|
||||
// If key is nil, signatures will carry a placeholder value
|
||||
func generatePluginArtifactContents(t *testing.T, pluginName, pluginVersion string, pluginType consts.PluginType,
|
||||
includeBinarySig bool, privKey *crypto.Key,
|
||||
) map[string][]byte {
|
||||
t.Helper()
|
||||
|
||||
metadata := pluginMetadata{
|
||||
Version: "v0",
|
||||
Plugin: Plugin{
|
||||
Name: pluginName,
|
||||
Type: pluginType,
|
||||
Tier: consts.PluginTierOfficial,
|
||||
By: "hashicorp",
|
||||
Version: pluginVersion,
|
||||
Platform: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
PGPSig: "signature-placeholder",
|
||||
},
|
||||
}
|
||||
metadataBytes, err := json.Marshal(metadata)
|
||||
assert.NoError(t, err)
|
||||
|
||||
pluginBytes := []byte("plugin-binary-placeholder")
|
||||
metadataSigBytes := []byte("signature-placeholder")
|
||||
pgp := crypto.PGP()
|
||||
if privKey != nil {
|
||||
signer, err := pgp.Sign().SigningKey(privKey).Detached().New()
|
||||
defer signer.ClearPrivateParams()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// exclude binary signature for bad plugin signature read test
|
||||
metadata.Plugin.PGPSig = ""
|
||||
if includeBinarySig {
|
||||
signature, err := signer.Sign(pluginBytes, crypto.Armor)
|
||||
assert.NoError(t, err)
|
||||
|
||||
metadata.Plugin.PGPSig = string(signature)
|
||||
}
|
||||
|
||||
metadataBytes, err = json.Marshal(metadata)
|
||||
assert.NoError(t, err)
|
||||
|
||||
metadataSigBytes, err = signer.Sign(metadataBytes, crypto.Armor)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
return map[string][]byte{
|
||||
metadataFile: metadataBytes,
|
||||
metadataSig: metadataSigBytes,
|
||||
pluginName: pluginBytes,
|
||||
"LICENSE": []byte("license-placeholder"),
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue