diff --git a/api/sys_auth.go b/api/sys_auth.go index ff479a20e1..fd55e429e6 100644 --- a/api/sys_auth.go +++ b/api/sys_auth.go @@ -85,6 +85,7 @@ type EnableAuthOptions struct { Type string `json:"type" structs:"type"` Description string `json:"description" structs:"description"` Local bool `json:"local" structs:"local"` + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` } type AuthMount struct { @@ -96,6 +97,7 @@ type AuthMount struct { } type AuthConfigOutput struct { - DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` - MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` + DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` + MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` } diff --git a/api/sys_mounts.go b/api/sys_mounts.go index d358f8d871..e0bb9ff0d5 100644 --- a/api/sys_mounts.go +++ b/api/sys_mounts.go @@ -130,6 +130,7 @@ type MountConfigInput struct { DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` } type MountOutput struct { @@ -141,7 +142,8 @@ type MountOutput struct { } type MountConfigOutput struct { - DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` - MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` - ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` + DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` + MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` + ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` } diff --git a/builtin/credential/app-id/backend.go b/builtin/credential/app-id/backend.go index eba7d136cd..e0a8f88b14 100644 --- a/builtin/credential/app-id/backend.go +++ b/builtin/credential/app-id/backend.go @@ -13,10 +13,13 @@ func Factory(conf *logical.BackendConfig) (logical.Backend, error) { if err != nil { return nil, err } - return b.Backend.Setup(conf) + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } -func Backend(conf *logical.BackendConfig) (backend, error) { +func Backend(conf *logical.BackendConfig) (*backend, error) { var b backend b.MapAppId = &framework.PolicyMap{ PathMap: framework.PathMap{ @@ -78,7 +81,7 @@ func Backend(conf *logical.BackendConfig) (backend, error) { b.MapAppId.SaltFunc = b.Salt b.MapUserId.SaltFunc = b.Salt - return b, nil + return &b, nil } type backend struct { diff --git a/builtin/credential/app-id/backend_test.go b/builtin/credential/app-id/backend_test.go index e795a816a2..4ae5d3e1cf 100644 --- a/builtin/credential/app-id/backend_test.go +++ b/builtin/credential/app-id/backend_test.go @@ -9,7 +9,7 @@ import ( ) func TestBackend_basic(t *testing.T) { - var b backend + var b *backend var err error var storage logical.Storage factory := func(conf *logical.BackendConfig) (logical.Backend, error) { @@ -18,7 +18,10 @@ func TestBackend_basic(t *testing.T) { t.Fatal(err) } storage = conf.StorageView - return b.Setup(conf) + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } logicaltest.Test(t, logicaltest.TestCase{ Factory: factory, diff --git a/builtin/credential/approle/backend.go b/builtin/credential/approle/backend.go index 46dae815d5..fe4053a29c 100644 --- a/builtin/credential/approle/backend.go +++ b/builtin/credential/approle/backend.go @@ -54,7 +54,10 @@ func Factory(conf *logical.BackendConfig) (logical.Backend, error) { if err != nil { return nil, err } - return b.Setup(conf) + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } func Backend(conf *logical.BackendConfig) (*backend, error) { diff --git a/builtin/credential/approle/backend_test.go b/builtin/credential/approle/backend_test.go index 2a3e3773ec..5f16e5f646 100644 --- a/builtin/credential/approle/backend_test.go +++ b/builtin/credential/approle/backend_test.go @@ -17,7 +17,7 @@ func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) { if b == nil { t.Fatalf("failed to create backend") } - _, err = b.Backend.Setup(config) + err = b.Backend.Setup(config) if err != nil { t.Fatal(err) } diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 4ffec58f07..f402552a51 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -17,7 +17,10 @@ func Factory(conf *logical.BackendConfig) (logical.Backend, error) { if err != nil { return nil, err } - return b.Setup(conf) + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } type backend struct { diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 67c4fb7ce4..b23d0ee24d 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -29,7 +29,7 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -253,7 +253,7 @@ func TestBackend_ConfigTidyIdentities(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -307,7 +307,7 @@ func TestBackend_ConfigTidyRoleTags(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -361,7 +361,7 @@ func TestBackend_TidyIdentities(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -386,7 +386,7 @@ func TestBackend_TidyRoleTags(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -411,7 +411,7 @@ func TestBackend_ConfigClient(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -548,7 +548,7 @@ func TestBackend_pathConfigCertificate(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -703,7 +703,7 @@ func TestBackend_parseAndVerifyRoleTagValue(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -784,7 +784,7 @@ func TestBackend_PathRoleTag(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -849,7 +849,7 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -997,7 +997,7 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -1177,7 +1177,7 @@ func TestBackend_pathStsConfig(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -1325,7 +1325,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } diff --git a/builtin/credential/aws/path_config_client_test.go b/builtin/credential/aws/path_config_client_test.go index 215cee75e7..ff60ebff64 100644 --- a/builtin/credential/aws/path_config_client_test.go +++ b/builtin/credential/aws/path_config_client_test.go @@ -15,7 +15,7 @@ func TestBackend_pathConfigClient(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } diff --git a/builtin/credential/aws/path_role_test.go b/builtin/credential/aws/path_role_test.go index 5e598ca96a..584b6dae7b 100644 --- a/builtin/credential/aws/path_role_test.go +++ b/builtin/credential/aws/path_role_test.go @@ -19,7 +19,7 @@ func TestBackend_pathRoleEc2(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -146,7 +146,7 @@ func Test_enableIamIDResolution(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -221,7 +221,7 @@ func TestBackend_pathIam(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -385,7 +385,7 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -491,7 +491,7 @@ func TestAwsEc2_RoleCrud(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } @@ -617,7 +617,7 @@ func TestAwsEc2_RoleDurationSeconds(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } diff --git a/builtin/credential/cert/backend.go b/builtin/credential/cert/backend.go index 088cc41ab1..5436581941 100644 --- a/builtin/credential/cert/backend.go +++ b/builtin/credential/cert/backend.go @@ -10,9 +10,8 @@ import ( func Factory(conf *logical.BackendConfig) (logical.Backend, error) { b := Backend() - _, err := b.Setup(conf) - if err != nil { - return b, err + if err := b.Setup(conf); err != nil { + return nil, err } return b, nil } diff --git a/builtin/credential/github/backend.go b/builtin/credential/github/backend.go index 2820422ac4..208aaaf472 100644 --- a/builtin/credential/github/backend.go +++ b/builtin/credential/github/backend.go @@ -11,7 +11,11 @@ import ( ) func Factory(conf *logical.BackendConfig) (logical.Backend, error) { - return Backend().Setup(conf) + b := Backend() + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } func Backend() *backend { diff --git a/builtin/credential/ldap/backend.go b/builtin/credential/ldap/backend.go index a241ce4a42..15456001e8 100644 --- a/builtin/credential/ldap/backend.go +++ b/builtin/credential/ldap/backend.go @@ -13,7 +13,11 @@ import ( ) func Factory(conf *logical.BackendConfig) (logical.Backend, error) { - return Backend().Setup(conf) + b := Backend() + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } func Backend() *backend { diff --git a/builtin/credential/ldap/backend_test.go b/builtin/credential/ldap/backend_test.go index e5dc2cbf93..d900c21ae7 100644 --- a/builtin/credential/ldap/backend_test.go +++ b/builtin/credential/ldap/backend_test.go @@ -21,7 +21,7 @@ func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) { t.Fatalf("failed to create backend") } - _, err := b.Backend.Setup(config) + err := b.Backend.Setup(config) if err != nil { t.Fatal(err) } diff --git a/builtin/credential/okta/backend.go b/builtin/credential/okta/backend.go index 7cadd945b4..3957ea5988 100644 --- a/builtin/credential/okta/backend.go +++ b/builtin/credential/okta/backend.go @@ -8,7 +8,11 @@ import ( ) func Factory(conf *logical.BackendConfig) (logical.Backend, error) { - return Backend().Setup(conf) + b := Backend() + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } func Backend() *backend { diff --git a/builtin/credential/radius/backend.go b/builtin/credential/radius/backend.go index 4bd3306967..6404b8c31c 100644 --- a/builtin/credential/radius/backend.go +++ b/builtin/credential/radius/backend.go @@ -7,7 +7,11 @@ import ( ) func Factory(conf *logical.BackendConfig) (logical.Backend, error) { - return Backend().Setup(conf) + b := Backend() + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } func Backend() *backend { diff --git a/builtin/credential/userpass/backend.go b/builtin/credential/userpass/backend.go index d21989505b..f87f5e3714 100644 --- a/builtin/credential/userpass/backend.go +++ b/builtin/credential/userpass/backend.go @@ -7,7 +7,11 @@ import ( ) func Factory(conf *logical.BackendConfig) (logical.Backend, error) { - return Backend().Setup(conf) + b := Backend() + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } func Backend() *backend { diff --git a/builtin/logical/aws/backend.go b/builtin/logical/aws/backend.go index 246e25cf24..a0121118cd 100644 --- a/builtin/logical/aws/backend.go +++ b/builtin/logical/aws/backend.go @@ -9,7 +9,11 @@ import ( ) func Factory(conf *logical.BackendConfig) (logical.Backend, error) { - return Backend().Setup(conf) + b := Backend() + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } func Backend() *backend { diff --git a/builtin/logical/aws/path_roles_test.go b/builtin/logical/aws/path_roles_test.go index 08bbca93a0..3314c7a3eb 100644 --- a/builtin/logical/aws/path_roles_test.go +++ b/builtin/logical/aws/path_roles_test.go @@ -14,7 +14,7 @@ func TestBackend_PathListRoles(t *testing.T) { config.StorageView = &logical.InmemStorage{} b := Backend() - if _, err := b.Setup(config); err != nil { + if err := b.Setup(config); err != nil { t.Fatal(err) } diff --git a/builtin/logical/cassandra/backend.go b/builtin/logical/cassandra/backend.go index c2e769ccb4..cd455a182f 100644 --- a/builtin/logical/cassandra/backend.go +++ b/builtin/logical/cassandra/backend.go @@ -12,7 +12,11 @@ import ( // Factory creates a new backend func Factory(conf *logical.BackendConfig) (logical.Backend, error) { - return Backend().Setup(conf) + b := Backend() + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } // Backend contains the base information for the backend's functionality diff --git a/builtin/logical/consul/backend.go b/builtin/logical/consul/backend.go index 0b4351f13f..cd19f636b6 100644 --- a/builtin/logical/consul/backend.go +++ b/builtin/logical/consul/backend.go @@ -6,7 +6,11 @@ import ( ) func Factory(conf *logical.BackendConfig) (logical.Backend, error) { - return Backend().Setup(conf) + b := Backend() + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } func Backend() *backend { diff --git a/builtin/logical/database/backend.go b/builtin/logical/database/backend.go index af76f9978a..fe227dd3db 100644 --- a/builtin/logical/database/backend.go +++ b/builtin/logical/database/backend.go @@ -16,7 +16,11 @@ import ( const databaseConfigPath = "database/config/" func Factory(conf *logical.BackendConfig) (logical.Backend, error) { - return Backend(conf).Setup(conf) + b := Backend(conf) + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } func Backend(conf *logical.BackendConfig) *databaseBackend { diff --git a/builtin/logical/mongodb/backend.go b/builtin/logical/mongodb/backend.go index e9f3b29add..52f4342122 100644 --- a/builtin/logical/mongodb/backend.go +++ b/builtin/logical/mongodb/backend.go @@ -12,7 +12,11 @@ import ( ) func Factory(conf *logical.BackendConfig) (logical.Backend, error) { - return Backend().Setup(conf) + b := Backend() + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } func Backend() *framework.Backend { diff --git a/builtin/logical/mssql/backend.go b/builtin/logical/mssql/backend.go index 61afe75d9c..44aac36d5a 100644 --- a/builtin/logical/mssql/backend.go +++ b/builtin/logical/mssql/backend.go @@ -12,7 +12,11 @@ import ( ) func Factory(conf *logical.BackendConfig) (logical.Backend, error) { - return Backend().Setup(conf) + b := Backend() + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } func Backend() *backend { diff --git a/builtin/logical/mysql/backend.go b/builtin/logical/mysql/backend.go index 7ae0335e1a..4e97330278 100644 --- a/builtin/logical/mysql/backend.go +++ b/builtin/logical/mysql/backend.go @@ -12,7 +12,11 @@ import ( ) func Factory(conf *logical.BackendConfig) (logical.Backend, error) { - return Backend().Setup(conf) + b := Backend() + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } func Backend() *backend { diff --git a/builtin/logical/pki/backend.go b/builtin/logical/pki/backend.go index 6128028346..ff2c74b637 100644 --- a/builtin/logical/pki/backend.go +++ b/builtin/logical/pki/backend.go @@ -11,7 +11,11 @@ import ( // Factory creates a new backend implementing the logical.Backend interface func Factory(conf *logical.BackendConfig) (logical.Backend, error) { - return Backend().Setup(conf) + b := Backend() + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } // Backend returns a new Backend framework struct diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go index 4fefc95c13..d50a0f40de 100644 --- a/builtin/logical/pki/backend_test.go +++ b/builtin/logical/pki/backend_test.go @@ -1870,7 +1870,7 @@ func TestBackend_PathFetchCertList(t *testing.T) { config.StorageView = storage b := Backend() - _, err := b.Setup(config) + err := b.Setup(config) if err != nil { t.Fatal(err) } @@ -1997,7 +1997,7 @@ func TestBackend_SignVerbatim(t *testing.T) { config.StorageView = storage b := Backend() - _, err := b.Setup(config) + err := b.Setup(config) if err != nil { t.Fatal(err) } diff --git a/builtin/logical/pki/path_roles_test.go b/builtin/logical/pki/path_roles_test.go index 1adfc9d1e0..bd0aa9049e 100644 --- a/builtin/logical/pki/path_roles_test.go +++ b/builtin/logical/pki/path_roles_test.go @@ -13,7 +13,7 @@ func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) { var err error b := Backend() - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } diff --git a/builtin/logical/postgresql/backend.go b/builtin/logical/postgresql/backend.go index 6f4befd8a1..2b9dac9759 100644 --- a/builtin/logical/postgresql/backend.go +++ b/builtin/logical/postgresql/backend.go @@ -13,7 +13,11 @@ import ( ) func Factory(conf *logical.BackendConfig) (logical.Backend, error) { - return Backend(conf).Setup(conf) + b := Backend(conf) + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } func Backend(conf *logical.BackendConfig) *backend { diff --git a/builtin/logical/rabbitmq/backend.go b/builtin/logical/rabbitmq/backend.go index 4f9cde0d7e..3ad8df6762 100644 --- a/builtin/logical/rabbitmq/backend.go +++ b/builtin/logical/rabbitmq/backend.go @@ -13,7 +13,11 @@ import ( // Factory creates and configures the backend func Factory(conf *logical.BackendConfig) (logical.Backend, error) { - return Backend().Setup(conf) + b := Backend() + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } // Creates a new backend with all the paths and secrets belonging to it diff --git a/builtin/logical/rabbitmq/path_config_lease_test.go b/builtin/logical/rabbitmq/path_config_lease_test.go index a5c99835c5..4182fd47b3 100644 --- a/builtin/logical/rabbitmq/path_config_lease_test.go +++ b/builtin/logical/rabbitmq/path_config_lease_test.go @@ -13,7 +13,7 @@ func TestBackend_config_lease_RU(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b := Backend() - if _, err = b.Setup(config); err != nil { + if err = b.Setup(config); err != nil { t.Fatal(err) } diff --git a/builtin/logical/ssh/backend.go b/builtin/logical/ssh/backend.go index 9c1f52b4bd..5633ac0c27 100644 --- a/builtin/logical/ssh/backend.go +++ b/builtin/logical/ssh/backend.go @@ -21,7 +21,10 @@ func Factory(conf *logical.BackendConfig) (logical.Backend, error) { if err != nil { return nil, err } - return b.Setup(conf) + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } func Backend(conf *logical.BackendConfig) (*backend, error) { diff --git a/builtin/logical/ssh/backend_test.go b/builtin/logical/ssh/backend_test.go index 814e2d2b47..139d24acad 100644 --- a/builtin/logical/ssh/backend_test.go +++ b/builtin/logical/ssh/backend_test.go @@ -106,7 +106,7 @@ func TestBackend_allowed_users(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } diff --git a/builtin/logical/ssh/path_config_ca_test.go b/builtin/logical/ssh/path_config_ca_test.go index cc0b17b7de..250ab4f29a 100644 --- a/builtin/logical/ssh/path_config_ca_test.go +++ b/builtin/logical/ssh/path_config_ca_test.go @@ -17,7 +17,7 @@ func TestSSH_ConfigCAStorageUpgrade(t *testing.T) { t.Fatal(err) } - _, err = b.Setup(config) + err = b.Setup(config) if err != nil { t.Fatal(err) } diff --git a/builtin/logical/totp/backend.go b/builtin/logical/totp/backend.go index e26926aa73..91a803879c 100644 --- a/builtin/logical/totp/backend.go +++ b/builtin/logical/totp/backend.go @@ -10,10 +10,14 @@ import ( ) func Factory(conf *logical.BackendConfig) (logical.Backend, error) { - return Backend(conf).Setup(conf) + b := Backend() + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil } -func Backend(conf *logical.BackendConfig) *backend { +func Backend() *backend { var b backend b.Backend = &framework.Backend{ Help: strings.TrimSpace(backendHelp), diff --git a/builtin/logical/transit/backend.go b/builtin/logical/transit/backend.go index 37ebca43e4..7857776e92 100644 --- a/builtin/logical/transit/backend.go +++ b/builtin/logical/transit/backend.go @@ -10,12 +10,10 @@ import ( func Factory(conf *logical.BackendConfig) (logical.Backend, error) { b := Backend(conf) - be, err := b.Backend.Setup(conf) - if err != nil { + if err := b.Setup(conf); err != nil { return nil, err } - - return be, nil + return b, nil } func Backend(conf *logical.BackendConfig) *backend { diff --git a/builtin/logical/transit/backend_test.go b/builtin/logical/transit/backend_test.go index 206054742f..a9c27bcef6 100644 --- a/builtin/logical/transit/backend_test.go +++ b/builtin/logical/transit/backend_test.go @@ -31,7 +31,7 @@ func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) { if b == nil { t.Fatalf("failed to create backend") } - _, err := b.Backend.Setup(config) + err := b.Backend.Setup(config) if err != nil { t.Fatal(err) } diff --git a/builtin/plugin/backend.go b/builtin/plugin/backend.go new file mode 100644 index 0000000000..1e311255b7 --- /dev/null +++ b/builtin/plugin/backend.go @@ -0,0 +1,39 @@ +package plugin + +import ( + "fmt" + + "github.com/hashicorp/vault/logical" + bplugin "github.com/hashicorp/vault/logical/plugin" +) + +// Factory returns a configured plugin logical.Backend. +func Factory(conf *logical.BackendConfig) (logical.Backend, error) { + _, ok := conf.Config["plugin_name"] + if !ok { + return nil, fmt.Errorf("plugin_name not provided") + } + b, err := Backend(conf) + if err != nil { + return nil, err + } + + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil +} + +// Backend returns an instance of the backend, either as a plugin if external +// or as a concrete implementation if builtin, casted as logical.Backend. +func Backend(conf *logical.BackendConfig) (logical.Backend, error) { + name := conf.Config["plugin_name"] + sys := conf.System + + b, err := bplugin.NewBackend(name, sys) + if err != nil { + return nil, err + } + + return b, nil +} diff --git a/builtin/plugin/backend_test.go b/builtin/plugin/backend_test.go new file mode 100644 index 0000000000..10e385e3dd --- /dev/null +++ b/builtin/plugin/backend_test.go @@ -0,0 +1,101 @@ +package plugin + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/hashicorp/vault/helper/pluginutil" + "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/plugin" + "github.com/hashicorp/vault/logical/plugin/mock" + "github.com/hashicorp/vault/vault" +) + +func TestBackend(t *testing.T) { + config, cleanup := testConfig(t) + defer cleanup() + + _, err := Backend(config) + if err != nil { + t.Fatal(err) + } +} + +func TestBackend_Factory(t *testing.T) { + config, cleanup := testConfig(t) + defer cleanup() + + _, err := Factory(config) + if err != nil { + t.Fatal(err) + } +} + +func TestBackend_PluginMain(t *testing.T) { + if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" { + return + } + + content := []byte(vault.TestClusterCACert) + tmpfile, err := ioutil.TempFile("", "test-cacert") + if err != nil { + t.Fatal(err) + } + + defer os.Remove(tmpfile.Name()) // clean up + + if _, err := tmpfile.Write(content); err != nil { + t.Fatal(err) + } + if err := tmpfile.Close(); err != nil { + t.Fatal(err) + } + + args := []string{"--ca-cert=" + tmpfile.Name()} + + apiClientMeta := &pluginutil.APIClientMeta{} + flags := apiClientMeta.FlagSet() + flags.Parse(args) + tlsConfig := apiClientMeta.GetTLSConfig() + tlsProviderFunc := pluginutil.VaultPluginTLSProvider(tlsConfig) + + err = plugin.Serve(&plugin.ServeOpts{ + BackendFactoryFunc: mock.Factory, + TLSProviderFunc: tlsProviderFunc, + }) + if err != nil { + t.Fatal(err) + } +} + +func testConfig(t *testing.T) (*logical.BackendConfig, func()) { + coreConfig := &vault.CoreConfig{} + + cluster := vault.NewTestCluster(t, coreConfig, true) + cluster.StartListeners() + cores := cluster.Cores + + cores[0].Handler.Handle("/", http.Handler(cores[0].Core)) + cores[1].Handler.Handle("/", http.Handler(cores[1].Core)) + cores[2].Handler.Handle("/", http.Handler(cores[2].Core)) + + core := cores[0] + + sys := vault.TestDynamicSystemView(core.Core) + + config := &logical.BackendConfig{ + Logger: nil, + System: sys, + Config: map[string]string{ + "plugin_name": "mock-plugin", + }, + } + + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMain") + + return config, func() { + cluster.CloseListeners() + } +} diff --git a/cli/commands.go b/cli/commands.go index 3fab33956c..9eee004aec 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -31,6 +31,7 @@ import ( "github.com/hashicorp/vault/builtin/logical/ssh" "github.com/hashicorp/vault/builtin/logical/totp" "github.com/hashicorp/vault/builtin/logical/transit" + "github.com/hashicorp/vault/builtin/plugin" "github.com/hashicorp/vault/audit" "github.com/hashicorp/vault/command" @@ -79,6 +80,7 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory { "ldap": credLdap.Factory, "okta": credOkta.Factory, "radius": credRadius.Factory, + "plugin": plugin.Factory, }, LogicalBackends: map[string]logical.Factory{ "aws": aws.Factory, @@ -94,6 +96,7 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory { "rabbitmq": rabbitmq.Factory, "database": database.Factory, "totp": totp.Factory, + "plugin": plugin.Factory, }, ShutdownCh: command.MakeShutdownCh(), SighupCh: command.MakeSighupCh(), diff --git a/command/auth_enable.go b/command/auth_enable.go index 81c7cce215..7cf69c394e 100644 --- a/command/auth_enable.go +++ b/command/auth_enable.go @@ -14,11 +14,12 @@ type AuthEnableCommand struct { } func (c *AuthEnableCommand) Run(args []string) int { - var description, path string + var description, path, pluginName string var local bool flags := c.Meta.FlagSet("auth-enable", meta.FlagSetDefault) flags.StringVar(&description, "description", "", "") flags.StringVar(&path, "path", "", "") + flags.StringVar(&pluginName, "plugin-name", "", "") flags.BoolVar(&local, "local", false, "") flags.Usage = func() { c.Ui.Error(c.Help()) } if err := flags.Parse(args); err != nil { @@ -36,8 +37,13 @@ func (c *AuthEnableCommand) Run(args []string) int { authType := args[0] // If no path is specified, we default the path to the backend type + // or use the plugin name if it's a plugin backend if path == "" { - path = authType + if authType == "plugin" { + path = pluginName + } else { + path = authType + } } client, err := c.Client() @@ -50,6 +56,7 @@ func (c *AuthEnableCommand) Run(args []string) int { if err := client.Sys().EnableAuthWithOptions(path, &api.EnableAuthOptions{ Type: authType, Description: description, + PluginName: pluginName, Local: local, }); err != nil { c.Ui.Error(fmt.Sprintf( @@ -89,6 +96,9 @@ Auth Enable Options: to the type of the mount. This will make the auth provider available at "/auth/" + -plugin-name Name of the auth plugin to use based from the name + in the plugin catalog. + -local Mark the mount as a local mount. Local mounts are not replicated nor (if a secondary) removed by replication. diff --git a/command/mount.go b/command/mount.go index eb2b53a671..c29d13e242 100644 --- a/command/mount.go +++ b/command/mount.go @@ -14,13 +14,14 @@ type MountCommand struct { } func (c *MountCommand) Run(args []string) int { - var description, path, defaultLeaseTTL, maxLeaseTTL string + var description, path, defaultLeaseTTL, maxLeaseTTL, pluginName string var local, forceNoCache bool flags := c.Meta.FlagSet("mount", meta.FlagSetDefault) flags.StringVar(&description, "description", "", "") flags.StringVar(&path, "path", "", "") flags.StringVar(&defaultLeaseTTL, "default-lease-ttl", "", "") flags.StringVar(&maxLeaseTTL, "max-lease-ttl", "", "") + flags.StringVar(&pluginName, "plugin-name", "", "") flags.BoolVar(&forceNoCache, "force-no-cache", false, "") flags.BoolVar(&local, "local", false, "") flags.Usage = func() { c.Ui.Error(c.Help()) } @@ -39,8 +40,13 @@ func (c *MountCommand) Run(args []string) int { mountType := args[0] // If no path is specified, we default the path to the backend type + // or use the plugin name if it's a plugin backend if path == "" { - path = mountType + if mountType == "plugin" { + path = pluginName + } else { + path = mountType + } } client, err := c.Client() @@ -57,6 +63,7 @@ func (c *MountCommand) Run(args []string) int { DefaultLeaseTTL: defaultLeaseTTL, MaxLeaseTTL: maxLeaseTTL, ForceNoCache: forceNoCache, + PluginName: pluginName, }, Local: local, } @@ -67,9 +74,14 @@ func (c *MountCommand) Run(args []string) int { return 2 } + mountPart := fmt.Sprintf("'%s'", mountType) + if mountType == "plugin" { + mountPart = fmt.Sprintf("plugin '%s'", pluginName) + } + c.Ui.Output(fmt.Sprintf( - "Successfully mounted '%s' at '%s'!", - mountType, path)) + "Successfully mounted %s at '%s'!", + mountPart, path)) return 0 } @@ -112,10 +124,12 @@ Mount Options: not affect caching of the underlying encrypted data storage. + -plugin-name Name of the plugin to mount based from the name + in the plugin catalog. + -local Mark the mount as a local mount. Local mounts are not replicated nor (if a secondary) removed by replication. - ` return strings.TrimSpace(helpText) } diff --git a/command/mounts.go b/command/mounts.go index 2ee1665e12..26157760db 100644 --- a/command/mounts.go +++ b/command/mounts.go @@ -42,9 +42,13 @@ func (c *MountsCommand) Run(args []string) int { } sort.Strings(paths) - columns := []string{"Path | Type | Accessor | Default TTL | Max TTL | Force No Cache | Replication Behavior | Description"} + columns := []string{"Path | Type | Accessor | Plugin | Default TTL | Max TTL | Force No Cache | Replication Behavior | Description"} for _, path := range paths { mount := mounts[path] + pluginName := "n/a" + if mount.Config.PluginName != "" { + pluginName = mount.Config.PluginName + } defTTL := "system" switch { case mount.Type == "system": @@ -68,7 +72,7 @@ func (c *MountsCommand) Run(args []string) int { replicatedBehavior = "local" } columns = append(columns, fmt.Sprintf( - "%s | %s | %s | %s | %s | %v | %s | %s", path, mount.Type, mount.Accessor, defTTL, maxTTL, + "%s | %s | %s | %s | %s | %s | %v | %s | %s", path, mount.Type, mount.Accessor, pluginName, defTTL, maxTTL, mount.Config.ForceNoCache, replicatedBehavior, mount.Description)) } diff --git a/command/rekey_test.go b/command/rekey_test.go index c199dd2fd8..6f12d7834b 100644 --- a/command/rekey_test.go +++ b/command/rekey_test.go @@ -199,8 +199,8 @@ func TestRekey_init_pgp(t *testing.T) { MaxLeaseTTLVal: time.Hour * 24 * 32, }, } - sysBE := vault.NewSystemBackend(core) - sysBackend, err := sysBE.Backend.Setup(bc) + sysBackend := vault.NewSystemBackend(core) + err := sysBackend.Backend.Setup(bc) if err != nil { t.Fatal(err) } diff --git a/helper/builtinplugins/builtin.go b/helper/builtinplugins/builtin.go index 2f956c8c24..62f1e3b93b 100644 --- a/helper/builtinplugins/builtin.go +++ b/helper/builtinplugins/builtin.go @@ -9,9 +9,11 @@ import ( "github.com/hashicorp/vault/plugins/database/postgresql" ) +// BuiltinFactory is the func signature that should be returned by +// the plugin's New() func. type BuiltinFactory func() (interface{}, error) -var plugins map[string]BuiltinFactory = map[string]BuiltinFactory{ +var plugins = map[string]BuiltinFactory{ // These four plugins all use the same mysql implementation but with // different username settings passed by the constructor. "mysql-database-plugin": mysql.New(mysql.MetadataLen, mysql.UsernameLen), @@ -26,11 +28,14 @@ var plugins map[string]BuiltinFactory = map[string]BuiltinFactory{ "hana-database-plugin": hana.New, } +// Get returns the BuiltinFactory func for a particular backend plugin +// from the plugins map. func Get(name string) (BuiltinFactory, bool) { f, ok := plugins[name] return f, ok } +// Keys returns the list of plugin names that are considered builtin plugins. func Keys() []string { keys := make([]string, len(plugins)) diff --git a/helper/pluginutil/runner.go b/helper/pluginutil/runner.go index 4b25ba16bb..d050e1013f 100644 --- a/helper/pluginutil/runner.go +++ b/helper/pluginutil/runner.go @@ -35,12 +35,12 @@ type LookRunnerUtil interface { // PluginRunner defines the metadata needed to run a plugin securely with // go-plugin. type PluginRunner struct { - Name string `json:"name"` - Command string `json:"command"` - Args []string `json:"args"` - Sha256 []byte `json:"sha256"` - Builtin bool `json:"builtin"` - BuiltinFactory func() (interface{}, error) `json:"-"` + Name string `json:"name" structs:"name"` + Command string `json:"command" structs:"command"` + Args []string `json:"args" structs:"args"` + Sha256 []byte `json:"sha256" structs:"sha256"` + Builtin bool `json:"builtin" structs:"builtin"` + BuiltinFactory func() (interface{}, error) `json:"-" structs:"-"` } // Run takes a wrapper instance, and the go-plugin paramaters and executes a diff --git a/helper/pluginutil/tls.go b/helper/pluginutil/tls.go index 546e0fa8c2..4b7eb6f775 100644 --- a/helper/pluginutil/tls.go +++ b/helper/pluginutil/tls.go @@ -126,7 +126,7 @@ func VaultPluginTLSProvider(apiTLSConfig *api.TLSConfig) func() (*tls.Config, er // Parse the JWT and retrieve the vault address wt, err := jws.ParseJWT([]byte(unwrapToken)) if err != nil { - return nil, errors.New(fmt.Sprintf("error decoding token: %s", err)) + return nil, fmt.Errorf("error decoding token: %s", err) } if wt == nil { return nil, errors.New("nil decoded token") @@ -146,7 +146,7 @@ func VaultPluginTLSProvider(apiTLSConfig *api.TLSConfig) func() (*tls.Config, er // Sanity check the value if _, err := url.Parse(vaultAddr); err != nil { - return nil, errors.New(fmt.Sprintf("error parsing the vault address: %s", err)) + return nil, fmt.Errorf("error parsing the vault address: %s", err) } // Unwrap the token @@ -165,7 +165,7 @@ func VaultPluginTLSProvider(apiTLSConfig *api.TLSConfig) func() (*tls.Config, er return nil, errwrap.Wrapf("error during token unwrap request: {{err}}", err) } if secret == nil { - return nil, errors.New("error during token unwrap request secret is nil") + return nil, errors.New("error during token unwrap request: secret is nil") } // Retrieve and parse the server's certificate diff --git a/logical/framework/backend.go b/logical/framework/backend.go index e94ea04528..477a926fc9 100644 --- a/logical/framework/backend.go +++ b/logical/framework/backend.go @@ -82,6 +82,12 @@ type Backend struct { // See the built-in AuthRenew helpers in lease.go for common callbacks. AuthRenew OperationFunc + // LicenseRegistration is called to register the license for a backend. + LicenseRegistration LicenseRegistrationFunc + + // Type is the logical.BackendType for the backend implementation + BackendType logical.BackendType + logger log.Logger system logical.SystemView once sync.Once @@ -107,6 +113,10 @@ type InitializeFunc func() error // InvalidateFunc is the callback for backend key invalidation. type InvalidateFunc func(string) +// LicenseRegistrationFunc is the callback for backend license registration. +type LicenseRegistrationFunc func(interface{}) error + +// HandleExistenceCheck is the logical.Backend implementation. func (b *Backend) HandleExistenceCheck(req *logical.Request) (checkFound bool, exists bool, err error) { b.once.Do(b.init) @@ -154,7 +164,7 @@ func (b *Backend) HandleExistenceCheck(req *logical.Request) (checkFound bool, e return } -// logical.Backend impl. +// HandleRequest is the logical.Backend implementation. func (b *Backend) HandleRequest(req *logical.Request) (*logical.Response, error) { b.once.Do(b.init) @@ -221,18 +231,11 @@ func (b *Backend) HandleRequest(req *logical.Request) (*logical.Response, error) return callback(req, &fd) } -// logical.Backend impl. +// SpecialPaths is the logical.Backend implementation. func (b *Backend) SpecialPaths() *logical.Paths { return b.PathsSpecial } -// Setup is used to initialize the backend with the initial backend configuration -func (b *Backend) Setup(config *logical.BackendConfig) (logical.Backend, error) { - b.logger = config.Logger - b.system = config.System - return b, nil -} - // Cleanup is used to release resources and prepare to stop the backend func (b *Backend) Cleanup() { if b.Clean != nil { @@ -240,6 +243,7 @@ func (b *Backend) Cleanup() { } } +// Initialize calls the backend's Init func if set. func (b *Backend) Initialize() error { if b.Init != nil { return b.Init() @@ -255,6 +259,13 @@ func (b *Backend) InvalidateKey(key string) { } } +// Setup is used to initialize the backend with the initial backend configuration +func (b *Backend) Setup(config *logical.BackendConfig) error { + b.logger = config.Logger + b.system = config.System + return nil +} + // Logger can be used to get the logger. If no logger has been set, // the logs will be discarded. func (b *Backend) Logger() log.Logger { @@ -265,11 +276,25 @@ func (b *Backend) Logger() log.Logger { return logformat.NewVaultLoggerWithWriter(ioutil.Discard, log.LevelOff) } +// System returns the backend's system view. func (b *Backend) System() logical.SystemView { return b.system } -// This method takes in the TTL and MaxTTL values provided by the user, +// Type returns the backend type +func (b *Backend) Type() logical.BackendType { + return b.BackendType +} + +// RegisterLicense performs backend license registration. +func (b *Backend) RegisterLicense(license interface{}) error { + if b.LicenseRegistration == nil { + return nil + } + return b.LicenseRegistration(license) +} + +// SanitizeTTLStr takes in the TTL and MaxTTL values provided by the user, // compares those with the SystemView values. If they are empty a value of 0 is // set, which will cause initial secret or LeaseExtend operations to use the // mount/system defaults. If they are set, their boundaries are validated. @@ -297,7 +322,8 @@ func (b *Backend) SanitizeTTLStr(ttlStr, maxTTLStr string) (ttl, maxTTL time.Dur return } -// Caps the boundaries of ttl and max_ttl values to the backend mount's max_ttl value. +// SanitizeTTL caps the boundaries of ttl and max_ttl values to the +// backend mount's max_ttl value. func (b *Backend) SanitizeTTL(ttl, maxTTL time.Duration) (time.Duration, time.Duration, error) { sysMaxTTL := b.System().MaxLeaseTTL() if ttl > sysMaxTTL { @@ -575,6 +601,7 @@ func (s *FieldSchema) DefaultOrZero() interface{} { return s.Type.Zero() } +// Zero returns the correct zero-value for a specific FieldType func (t FieldType) Zero() interface{} { switch t { case TypeString: diff --git a/logical/logical.go b/logical/logical.go index 3b66fba51f..9ce0d85e78 100644 --- a/logical/logical.go +++ b/logical/logical.go @@ -2,6 +2,29 @@ package logical import log "github.com/mgutz/logxi/v1" +// BackendType is the type of backend that is being implemented +type BackendType uint32 + +// The these are the types of backends that can be derived from +// logical.Backend +const ( + TypeUnknown BackendType = 0 // This is also the zero-value for BackendType + TypeLogical BackendType = 1 + TypeCredential BackendType = 2 +) + +// Stringer implementation +func (b BackendType) String() string { + switch b { + case TypeLogical: + return "secret" + case TypeCredential: + return "auth" + } + + return "unknown" +} + // Backend interface must be implemented to be "mountable" at // a given path. Requests flow through a router which has various mount // points that flow to a logical backend. The logic of each backend is flexible, @@ -27,6 +50,11 @@ type Backend interface { // information, such as globally configured default and max lease TTLs. System() SystemView + // Logger provides an interface to access the underlying logger. This + // is useful when a struct embeds a Backend-implemented struct that + // contains a private instance of logger. + Logger() log.Logger + // HandleExistenceCheck is used to handle a request and generate a response // indicating whether the given path exists or not; this is used to // understand whether the request must have a Create or Update capability @@ -47,6 +75,16 @@ type Backend interface { // to the backend. The backend can use this to clear any caches or reset // internal state as needed. InvalidateKey(key string) + + // Setup is used to set up the backend based on the provided backend + // configuration. + Setup(*BackendConfig) error + + // Type returns the BackendType for the particular backend + Type() BackendType + + // RegisterLicense performs backend license registration + RegisterLicense(interface{}) error } // BackendConfig is provided to the factory to initialize the backend diff --git a/logical/plugin/backend.go b/logical/plugin/backend.go new file mode 100644 index 0000000000..8a80be0298 --- /dev/null +++ b/logical/plugin/backend.go @@ -0,0 +1,23 @@ +package plugin + +import ( + "net/rpc" + + "github.com/hashicorp/go-plugin" + "github.com/hashicorp/vault/logical" +) + +// BackendPlugin is the plugin.Plugin implementation +type BackendPlugin struct { + Factory func(*logical.BackendConfig) (logical.Backend, error) +} + +// Server gets called when on plugin.Serve() +func (b *BackendPlugin) Server(broker *plugin.MuxBroker) (interface{}, error) { + return &backendPluginServer{factory: b.Factory, broker: broker}, nil +} + +// Client gets called on plugin.NewClient() +func (b BackendPlugin) Client(broker *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { + return &backendPluginClient{client: c, broker: broker}, nil +} diff --git a/logical/plugin/backend_client.go b/logical/plugin/backend_client.go new file mode 100644 index 0000000000..a874c2416e --- /dev/null +++ b/logical/plugin/backend_client.go @@ -0,0 +1,228 @@ +package plugin + +import ( + "net/rpc" + + "github.com/hashicorp/go-plugin" + "github.com/hashicorp/vault/logical" + log "github.com/mgutz/logxi/v1" +) + +// backendPluginClient implements logical.Backend and is the +// go-plugin client. +type backendPluginClient struct { + broker *plugin.MuxBroker + client *rpc.Client + pluginClient *plugin.Client + + system logical.SystemView + logger log.Logger +} + +// HandleRequestArgs is the args for HandleRequest method. +type HandleRequestArgs struct { + StorageID uint32 + Request *logical.Request +} + +// HandleRequestReply is the reply for HandleRequest method. +type HandleRequestReply struct { + Response *logical.Response + Error *plugin.BasicError +} + +// SpecialPathsReply is the reply for SpecialPaths method. +type SpecialPathsReply struct { + Paths *logical.Paths +} + +// SystemReply is the reply for System method. +type SystemReply struct { + SystemView logical.SystemView + Error *plugin.BasicError +} + +// HandleExistenceCheckArgs is the args for HandleExistenceCheck method. +type HandleExistenceCheckArgs struct { + StorageID uint32 + Request *logical.Request +} + +// HandleExistenceCheckReply is the reply for HandleExistenceCheck method. +type HandleExistenceCheckReply struct { + CheckFound bool + Exists bool + Error *plugin.BasicError +} + +// SetupArgs is the args for Setup method. +type SetupArgs struct { + StorageID uint32 + LoggerID uint32 + SysViewID uint32 + Config map[string]string +} + +// SetupReply is the reply for Setup method. +type SetupReply struct { + Error *plugin.BasicError +} + +// TypeReply is the reply for the Type method. +type TypeReply struct { + Type logical.BackendType +} + +// RegisterLicenseArgs is the args for the RegisterLicense method. +type RegisterLicenseArgs struct { + License interface{} +} + +// RegisterLicenseReply is the reply for the RegisterLicense method. +type RegisterLicenseReply struct { + Error *plugin.BasicError +} + +func (b *backendPluginClient) HandleRequest(req *logical.Request) (*logical.Response, error) { + args := &HandleRequestArgs{ + Request: req, + } + var reply HandleRequestReply + + err := b.client.Call("Plugin.HandleRequest", args, &reply) + if err != nil { + return nil, err + } + if reply.Error != nil { + if reply.Error.Error() == logical.ErrUnsupportedOperation.Error() { + return nil, logical.ErrUnsupportedOperation + } + return nil, reply.Error + } + + return reply.Response, nil +} + +func (b *backendPluginClient) SpecialPaths() *logical.Paths { + var reply SpecialPathsReply + err := b.client.Call("Plugin.SpecialPaths", new(interface{}), &reply) + if err != nil { + return nil + } + + return reply.Paths +} + +// System returns vault's system view. The backend client stores the view during +// Setup, so there is no need to shim the system just to get it back. +func (b *backendPluginClient) System() logical.SystemView { + return b.system +} + +// Logger returns vault's logger. The backend client stores the logger during +// Setup, so there is no need to shim the logger just to get it back. +func (b *backendPluginClient) Logger() log.Logger { + return b.logger +} + +func (b *backendPluginClient) HandleExistenceCheck(req *logical.Request) (bool, bool, error) { + args := &HandleExistenceCheckArgs{ + Request: req, + } + var reply HandleExistenceCheckReply + + err := b.client.Call("Plugin.HandleExistenceCheck", args, &reply) + if err != nil { + return false, false, err + } + if reply.Error != nil { + // THINKING: Should be be a switch on all error types? + if reply.Error.Error() == logical.ErrUnsupportedPath.Error() { + return false, false, logical.ErrUnsupportedPath + } + return false, false, reply.Error + } + + return reply.CheckFound, reply.Exists, nil +} + +func (b *backendPluginClient) Cleanup() { + b.client.Call("Plugin.Cleanup", new(interface{}), &struct{}{}) +} + +func (b *backendPluginClient) Initialize() error { + err := b.client.Call("Plugin.Initialize", new(interface{}), &struct{}{}) + return err +} + +func (b *backendPluginClient) InvalidateKey(key string) { + b.client.Call("Plugin.InvalidateKey", key, &struct{}{}) +} + +func (b *backendPluginClient) Setup(config *logical.BackendConfig) error { + // Shim logical.Storage + storageID := b.broker.NextId() + go b.broker.AcceptAndServe(storageID, &StorageServer{ + impl: config.StorageView, + }) + + // Shim log.Logger + loggerID := b.broker.NextId() + go b.broker.AcceptAndServe(loggerID, &LoggerServer{ + logger: config.Logger, + }) + + // Shim logical.SystemView + sysViewID := b.broker.NextId() + go b.broker.AcceptAndServe(sysViewID, &SystemViewServer{ + impl: config.System, + }) + + args := &SetupArgs{ + StorageID: storageID, + LoggerID: loggerID, + SysViewID: sysViewID, + Config: config.Config, + } + var reply SetupReply + + err := b.client.Call("Plugin.Setup", args, &reply) + if err != nil { + return err + } + if reply.Error != nil { + return reply.Error + } + + // Set system and logger for getter methods + b.system = config.System + b.logger = config.Logger + + return nil +} + +func (b *backendPluginClient) Type() logical.BackendType { + var reply TypeReply + err := b.client.Call("Plugin.Type", new(interface{}), &reply) + if err != nil { + return logical.TypeUnknown + } + + return logical.BackendType(reply.Type) +} + +func (b *backendPluginClient) RegisterLicense(license interface{}) error { + var reply RegisterLicenseReply + args := RegisterLicenseArgs{ + License: license, + } + err := b.client.Call("Plugin.RegisterLicense", args, &reply) + if err != nil { + return err + } + if reply.Error != nil { + return reply.Error + } + + return nil +} diff --git a/logical/plugin/backend_server.go b/logical/plugin/backend_server.go new file mode 100644 index 0000000000..335bfa5f73 --- /dev/null +++ b/logical/plugin/backend_server.go @@ -0,0 +1,156 @@ +package plugin + +import ( + "net/rpc" + + "github.com/hashicorp/go-plugin" + "github.com/hashicorp/vault/logical" +) + +// backendPluginServer is the RPC server that backendPluginClient talks to, +// it methods conforming to requirements by net/rpc +type backendPluginServer struct { + broker *plugin.MuxBroker + backend logical.Backend + factory func(*logical.BackendConfig) (logical.Backend, error) + + loggerClient *rpc.Client + sysViewClient *rpc.Client + storageClient *rpc.Client +} + +func (b *backendPluginServer) HandleRequest(args *HandleRequestArgs, reply *HandleRequestReply) error { + storage := &StorageClient{client: b.storageClient} + args.Request.Storage = storage + + resp, err := b.backend.HandleRequest(args.Request) + *reply = HandleRequestReply{ + Response: resp, + Error: plugin.NewBasicError(err), + } + + return nil +} + +func (b *backendPluginServer) SpecialPaths(_ interface{}, reply *SpecialPathsReply) error { + *reply = SpecialPathsReply{ + Paths: b.backend.SpecialPaths(), + } + return nil +} + +func (b *backendPluginServer) HandleExistenceCheck(args *HandleExistenceCheckArgs, reply *HandleExistenceCheckReply) error { + storage := &StorageClient{client: b.storageClient} + args.Request.Storage = storage + + checkFound, exists, err := b.backend.HandleExistenceCheck(args.Request) + *reply = HandleExistenceCheckReply{ + CheckFound: checkFound, + Exists: exists, + Error: plugin.NewBasicError(err), + } + + return nil +} + +func (b *backendPluginServer) Cleanup(_ interface{}, _ *struct{}) error { + b.backend.Cleanup() + + // Close rpc clients + b.loggerClient.Close() + b.sysViewClient.Close() + b.storageClient.Close() + return nil +} + +func (b *backendPluginServer) Initialize(_ interface{}, _ *struct{}) error { + err := b.backend.Initialize() + return err +} + +func (b *backendPluginServer) InvalidateKey(args string, _ *struct{}) error { + b.backend.InvalidateKey(args) + return nil +} + +// Setup dials into the plugin's broker to get a shimmed storage, logger, and +// system view of the backend. This method also instantiates the underlying +// backend through its factory func for the server side of the plugin. +func (b *backendPluginServer) Setup(args *SetupArgs, reply *SetupReply) error { + // Dial for storage + storageConn, err := b.broker.Dial(args.StorageID) + if err != nil { + *reply = SetupReply{ + Error: plugin.NewBasicError(err), + } + return nil + } + rawStorageClient := rpc.NewClient(storageConn) + b.storageClient = rawStorageClient + + storage := &StorageClient{client: rawStorageClient} + + // Dial for logger + loggerConn, err := b.broker.Dial(args.LoggerID) + if err != nil { + *reply = SetupReply{ + Error: plugin.NewBasicError(err), + } + return nil + } + rawLoggerClient := rpc.NewClient(loggerConn) + b.loggerClient = rawLoggerClient + + logger := &LoggerClient{client: rawLoggerClient} + + // Dial for sys view + sysViewConn, err := b.broker.Dial(args.SysViewID) + if err != nil { + *reply = SetupReply{ + Error: plugin.NewBasicError(err), + } + return nil + } + rawSysViewClient := rpc.NewClient(sysViewConn) + b.sysViewClient = rawSysViewClient + + sysView := &SystemViewClient{client: rawSysViewClient} + + config := &logical.BackendConfig{ + StorageView: storage, + Logger: logger, + System: sysView, + Config: args.Config, + } + + // Call the underlying backend factory after shims have been created + // to set b.backend + backend, err := b.factory(config) + if err != nil { + *reply = SetupReply{ + Error: plugin.NewBasicError(err), + } + } + b.backend = backend + + return nil +} + +func (b *backendPluginServer) Type(_ interface{}, reply *TypeReply) error { + *reply = TypeReply{ + Type: b.backend.Type(), + } + + return nil +} + +func (b *backendPluginServer) RegisterLicense(args *RegisterLicenseArgs, reply *RegisterLicenseReply) error { + err := b.backend.RegisterLicense(args.License) + if err != nil { + *reply = RegisterLicenseReply{ + Error: plugin.NewBasicError(err), + } + } + + return nil +} diff --git a/logical/plugin/backend_test.go b/logical/plugin/backend_test.go new file mode 100644 index 0000000000..910c1c6938 --- /dev/null +++ b/logical/plugin/backend_test.go @@ -0,0 +1,176 @@ +package plugin + +import ( + "testing" + "time" + + gplugin "github.com/hashicorp/go-plugin" + "github.com/hashicorp/vault/helper/logformat" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/plugin/mock" + log "github.com/mgutz/logxi/v1" +) + +func TestBackendPlugin_impl(t *testing.T) { + var _ gplugin.Plugin = new(BackendPlugin) + var _ logical.Backend = new(backendPluginClient) +} + +func TestBackendPlugin_HandleRequest(t *testing.T) { + b, cleanup := testBackend(t) + defer cleanup() + + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "test/ing", + Data: map[string]interface{}{"value": "foo"}, + }) + if err != nil { + t.Fatal(err) + } + if resp.Data["value"] != "foo" { + t.Fatalf("bad: %#v", resp) + } +} + +func TestBackendPlugin_SpecialPaths(t *testing.T) { + b, cleanup := testBackend(t) + defer cleanup() + + paths := b.SpecialPaths() + if paths == nil { + t.Fatal("SpecialPaths() returned nil") + } +} + +func TestBackendPlugin_System(t *testing.T) { + b, cleanup := testBackend(t) + defer cleanup() + + sys := b.System() + if sys == nil { + t.Fatal("System() returned nil") + } + + actual := sys.DefaultLeaseTTL() + expected := 300 * time.Second + + if actual != expected { + t.Fatalf("bad: %v, expected %v", actual, expected) + } +} + +func TestBackendPlugin_Logger(t *testing.T) { + b, cleanup := testBackend(t) + defer cleanup() + + logger := b.Logger() + if logger == nil { + t.Fatal("Logger() returned nil") + } +} + +func TestBackendPlugin_HandleExistenceCheck(t *testing.T) { + b, cleanup := testBackend(t) + defer cleanup() + + checkFound, exists, err := b.HandleExistenceCheck(&logical.Request{ + Operation: logical.CreateOperation, + Path: "test/ing", + Data: map[string]interface{}{"value": "foo"}, + }) + if err != nil { + t.Fatal(err) + } + if !checkFound { + t.Fatal("existence check not found for path 'test/ing'") + } + if exists { + t.Fatal("existence check should have returned 'false' for 'testing/read'") + } +} + +func TestBackendPlugin_Cleanup(t *testing.T) { + b, cleanup := testBackend(t) + defer cleanup() + + b.Cleanup() +} + +func TestBackendPlugin_Initialize(t *testing.T) { + b, cleanup := testBackend(t) + defer cleanup() + + err := b.Initialize() + if err != nil { + t.Fatal(err) + } +} + +func TestBackendPlugin_InvalidateKey(t *testing.T) { + b, cleanup := testBackend(t) + defer cleanup() + + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "internal", + }) + if err != nil { + t.Fatal(err) + } + if resp.Data["value"] == "" { + t.Fatalf("bad: %#v, expected non-empty value", resp) + } + + b.InvalidateKey("internal") + + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "internal", + }) + if err != nil { + t.Fatal(err) + } + if resp.Data["value"] != "" { + t.Fatalf("bad: expected empty response data, got %#v", resp) + } +} + +func TestBackendPlugin_Setup(t *testing.T) { + _, cleanup := testBackend(t) + defer cleanup() +} + +func testBackend(t *testing.T) (logical.Backend, func()) { + // Create a mock provider + pluginMap := map[string]gplugin.Plugin{ + "backend": &BackendPlugin{ + Factory: mock.Factory, + }, + } + client, _ := gplugin.TestPluginRPCConn(t, pluginMap) + cleanup := func() { + client.Close() + } + + // Request the backend + raw, err := client.Dispense(BackendPluginName) + if err != nil { + t.Fatal(err) + } + b := raw.(logical.Backend) + + err = b.Setup(&logical.BackendConfig{ + Logger: logformat.NewVaultLogger(log.LevelTrace), + System: &logical.StaticSystemView{ + DefaultLeaseTTLVal: 300 * time.Second, + MaxLeaseTTLVal: 1800 * time.Second, + }, + StorageView: &logical.InmemStorage{}, + }) + if err != nil { + t.Fatal(err) + } + + return b, cleanup +} diff --git a/logical/plugin/logger.go b/logical/plugin/logger.go new file mode 100644 index 0000000000..ceb8947977 --- /dev/null +++ b/logical/plugin/logger.go @@ -0,0 +1,205 @@ +package plugin + +import ( + "net/rpc" + + plugin "github.com/hashicorp/go-plugin" + log "github.com/mgutz/logxi/v1" +) + +type LoggerClient struct { + client *rpc.Client +} + +func (l *LoggerClient) Trace(msg string, args ...interface{}) { + cArgs := &LoggerArgs{ + Msg: msg, + Args: args, + } + l.client.Call("Plugin.Trace", cArgs, &struct{}{}) +} + +func (l *LoggerClient) Debug(msg string, args ...interface{}) { + cArgs := &LoggerArgs{ + Msg: msg, + Args: args, + } + l.client.Call("Plugin.Debug", cArgs, &struct{}{}) +} + +func (l *LoggerClient) Info(msg string, args ...interface{}) { + cArgs := &LoggerArgs{ + Msg: msg, + Args: args, + } + l.client.Call("Plugin.Info", cArgs, &struct{}{}) +} +func (l *LoggerClient) Warn(msg string, args ...interface{}) error { + var reply LoggerReply + cArgs := &LoggerArgs{ + Msg: msg, + Args: args, + } + err := l.client.Call("Plugin.Warn", cArgs, &reply) + if err != nil { + return err + } + if reply.Error != nil { + return reply.Error + } + + return nil +} +func (l *LoggerClient) Error(msg string, args ...interface{}) error { + var reply LoggerReply + cArgs := &LoggerArgs{ + Msg: msg, + Args: args, + } + err := l.client.Call("Plugin.Error", cArgs, &reply) + if err != nil { + return err + } + if reply.Error != nil { + return reply.Error + } + + return nil +} + +func (l *LoggerClient) Fatal(msg string, args ...interface{}) { + // NOOP since it's not actually used within vault + return +} + +func (l *LoggerClient) Log(level int, msg string, args []interface{}) { + cArgs := &LoggerArgs{ + Level: level, + Msg: msg, + Args: args, + } + l.client.Call("Plugin.Log", cArgs, &struct{}{}) +} + +func (l *LoggerClient) SetLevel(level int) { + l.client.Call("Plugin.SetLevel", level, &struct{}{}) +} + +func (l *LoggerClient) IsTrace() bool { + var reply LoggerReply + l.client.Call("Plugin.IsTrace", new(interface{}), &reply) + return reply.IsTrue +} +func (l *LoggerClient) IsDebug() bool { + var reply LoggerReply + l.client.Call("Plugin.IsDebug", new(interface{}), &reply) + return reply.IsTrue +} + +func (l *LoggerClient) IsInfo() bool { + var reply LoggerReply + l.client.Call("Plugin.IsInfo", new(interface{}), &reply) + return reply.IsTrue +} + +func (l *LoggerClient) IsWarn() bool { + var reply LoggerReply + l.client.Call("Plugin.IsWarn", new(interface{}), &reply) + return reply.IsTrue +} + +type LoggerServer struct { + logger log.Logger +} + +func (l *LoggerServer) Trace(args *LoggerArgs, _ *struct{}) error { + l.logger.Trace(args.Msg, args.Args) + return nil +} + +func (l *LoggerServer) Debug(args *LoggerArgs, _ *struct{}) error { + l.logger.Debug(args.Msg, args.Args) + return nil +} + +func (l *LoggerServer) Info(args *LoggerArgs, _ *struct{}) error { + l.logger.Info(args.Msg, args.Args) + return nil +} + +func (l *LoggerServer) Warn(args *LoggerArgs, reply *LoggerReply) error { + err := l.logger.Warn(args.Msg, args.Args) + if err != nil { + *reply = LoggerReply{ + Error: plugin.NewBasicError(err), + } + return nil + } + return nil +} + +func (l *LoggerServer) Error(args *LoggerArgs, reply *LoggerReply) error { + err := l.logger.Error(args.Msg, args.Args) + if err != nil { + *reply = LoggerReply{ + Error: plugin.NewBasicError(err), + } + return nil + } + return nil +} + +func (l *LoggerServer) Log(args *LoggerArgs, _ *struct{}) error { + l.logger.Log(args.Level, args.Msg, args.Args) + return nil +} + +func (l *LoggerServer) SetLevel(args int, _ *struct{}) error { + l.logger.SetLevel(args) + return nil +} + +func (l *LoggerServer) IsTrace(args interface{}, reply *LoggerReply) error { + result := l.logger.IsTrace() + *reply = LoggerReply{ + IsTrue: result, + } + return nil +} + +func (l *LoggerServer) IsDebug(args interface{}, reply *LoggerReply) error { + result := l.logger.IsDebug() + *reply = LoggerReply{ + IsTrue: result, + } + return nil +} + +func (l *LoggerServer) IsInfo(args interface{}, reply *LoggerReply) error { + result := l.logger.IsInfo() + *reply = LoggerReply{ + IsTrue: result, + } + return nil +} + +func (l *LoggerServer) IsWarn(args interface{}, reply *LoggerReply) error { + result := l.logger.IsWarn() + *reply = LoggerReply{ + IsTrue: result, + } + return nil +} + +type LoggerArgs struct { + Level int + Msg string + Args []interface{} +} + +// LoggerReply contains the RPC reply. Not all fields may be used +// for a particular RPC call. +type LoggerReply struct { + IsTrue bool + Error *plugin.BasicError +} diff --git a/logical/plugin/logger_test.go b/logical/plugin/logger_test.go new file mode 100644 index 0000000000..10b389c69a --- /dev/null +++ b/logical/plugin/logger_test.go @@ -0,0 +1,163 @@ +package plugin + +import ( + "bufio" + "bytes" + "io/ioutil" + "strings" + "testing" + + plugin "github.com/hashicorp/go-plugin" + "github.com/hashicorp/vault/helper/logformat" + log "github.com/mgutz/logxi/v1" +) + +func TestLogger_impl(t *testing.T) { + var _ log.Logger = new(LoggerClient) +} + +func TestLogger_levels(t *testing.T) { + client, server := plugin.TestRPCConn(t) + defer client.Close() + + var buf bytes.Buffer + writer := bufio.NewWriter(&buf) + + l := logformat.NewVaultLoggerWithWriter(writer, log.LevelTrace) + + server.RegisterName("Plugin", &LoggerServer{ + logger: l, + }) + + expected := "foobar" + testLogger := &LoggerClient{client: client} + + // Test trace + testLogger.Trace(expected) + if err := writer.Flush(); err != nil { + t.Fatal(err) + } + result := buf.String() + buf.Reset() + if !strings.Contains(result, expected) { + t.Fatalf("expected log to contain %s, got %s", expected, result) + } + + // Test debug + testLogger.Debug(expected) + if err := writer.Flush(); err != nil { + t.Fatal(err) + } + result = buf.String() + buf.Reset() + if !strings.Contains(result, expected) { + t.Fatalf("expected log to contain %s, got %s", expected, result) + } + + // Test debug + testLogger.Info(expected) + if err := writer.Flush(); err != nil { + t.Fatal(err) + } + result = buf.String() + buf.Reset() + if !strings.Contains(result, expected) { + t.Fatalf("expected log to contain %s, got %s", expected, result) + } + + // Test warn + testLogger.Warn(expected) + if err := writer.Flush(); err != nil { + t.Fatal(err) + } + result = buf.String() + buf.Reset() + if !strings.Contains(result, expected) { + t.Fatalf("expected log to contain %s, got %s", expected, result) + } + + // Test error + testLogger.Error(expected) + if err := writer.Flush(); err != nil { + t.Fatal(err) + } + result = buf.String() + buf.Reset() + if !strings.Contains(result, expected) { + t.Fatalf("expected log to contain %s, got %s", expected, result) + } + + // Test fatal + testLogger.Fatal(expected) + if err := writer.Flush(); err != nil { + t.Fatal(err) + } + result = buf.String() + buf.Reset() + if result != "" { + t.Fatalf("expected log Fatal() to be no-op, got %s", result) + } +} + +func TestLogger_isLevels(t *testing.T) { + client, server := plugin.TestRPCConn(t) + defer client.Close() + + l := logformat.NewVaultLoggerWithWriter(ioutil.Discard, log.LevelAll) + + server.RegisterName("Plugin", &LoggerServer{ + logger: l, + }) + + testLogger := &LoggerClient{client: client} + + if !testLogger.IsDebug() || !testLogger.IsInfo() || !testLogger.IsTrace() || !testLogger.IsWarn() { + t.Fatal("expected logger to return true for all logger level checks") + } +} + +func TestLogger_log(t *testing.T) { + client, server := plugin.TestRPCConn(t) + defer client.Close() + + var buf bytes.Buffer + writer := bufio.NewWriter(&buf) + + l := logformat.NewVaultLoggerWithWriter(writer, log.LevelTrace) + + server.RegisterName("Plugin", &LoggerServer{ + logger: l, + }) + + expected := "foobar" + testLogger := &LoggerClient{client: client} + + // Test trace + testLogger.Log(log.LevelInfo, expected, nil) + if err := writer.Flush(); err != nil { + t.Fatal(err) + } + result := buf.String() + if !strings.Contains(result, expected) { + t.Fatalf("expected log to contain %s, got %s", expected, result) + } + +} + +func TestLogger_setLevel(t *testing.T) { + client, server := plugin.TestRPCConn(t) + defer client.Close() + + l := log.NewLogger(ioutil.Discard, "test-logger") + + server.RegisterName("Plugin", &LoggerServer{ + logger: l, + }) + + testLogger := &LoggerClient{client: client} + testLogger.SetLevel(log.LevelWarn) + + if !testLogger.IsWarn() { + t.Fatal("expected logger to support warn level") + } +} diff --git a/logical/plugin/mock/backend.go b/logical/plugin/mock/backend.go new file mode 100644 index 0000000000..a9a2efac8d --- /dev/null +++ b/logical/plugin/mock/backend.go @@ -0,0 +1,69 @@ +package mock + +import ( + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +// New returns a new backend as an interface. This func +// is only necessary for builtin backend plugins. +func New() (interface{}, error) { + return Backend(), nil +} + +// Factory returns a new backend as logical.Backend. +func Factory(conf *logical.BackendConfig) (logical.Backend, error) { + b := Backend() + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil +} + +// FactoryType is a wrapper func that allows the Factory func to specify +// the backend type for the mock backend plugin instance. +func FactoryType(backendType logical.BackendType) func(*logical.BackendConfig) (logical.Backend, error) { + return func(conf *logical.BackendConfig) (logical.Backend, error) { + b := Backend() + b.BackendType = backendType + if err := b.Setup(conf); err != nil { + return nil, err + } + return b, nil + } +} + +// Backend returns a private embedded struct of framework.Backend. +func Backend() *backend { + var b backend + b.Backend = &framework.Backend{ + Help: "", + Paths: []*framework.Path{ + pathTesting(&b), + pathInternal(&b), + }, + PathsSpecial: &logical.Paths{ + Unauthenticated: []string{ + "special", + }, + }, + Secrets: []*framework.Secret{}, + Invalidate: b.invalidate, + } + b.internal = "bar" + return &b +} + +type backend struct { + *framework.Backend + + // internal is used to test invalidate + internal string +} + +func (b *backend) invalidate(key string) { + switch key { + case "internal": + b.internal = "" + } +} diff --git a/logical/plugin/mock/backend_test.go b/logical/plugin/mock/backend_test.go new file mode 100644 index 0000000000..81cf7dba35 --- /dev/null +++ b/logical/plugin/mock/backend_test.go @@ -0,0 +1,11 @@ +package mock + +import ( + "testing" + + "github.com/hashicorp/vault/logical" +) + +func TestMockBackend_impl(t *testing.T) { + var _ logical.Backend = new(backend) +} diff --git a/logical/plugin/mock/mock-plugin/main.go b/logical/plugin/mock/mock-plugin/main.go new file mode 100644 index 0000000000..d8af7db615 --- /dev/null +++ b/logical/plugin/mock/mock-plugin/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "log" + "os" + + "github.com/hashicorp/vault/helper/pluginutil" + "github.com/hashicorp/vault/logical/plugin" + "github.com/hashicorp/vault/logical/plugin/mock" +) + +func main() { + apiClientMeta := &pluginutil.APIClientMeta{} + flags := apiClientMeta.FlagSet() + flags.Parse(os.Args) + + tlsConfig := apiClientMeta.GetTLSConfig() + tlsProviderFunc := pluginutil.VaultPluginTLSProvider(tlsConfig) + + err := plugin.Serve(&plugin.ServeOpts{ + BackendFactoryFunc: mock.Factory, + TLSProviderFunc: tlsProviderFunc, + }) + if err != nil { + log.Println(err) + os.Exit(1) + } +} diff --git a/logical/plugin/mock/path_internal.go b/logical/plugin/mock/path_internal.go new file mode 100644 index 0000000000..b59b104941 --- /dev/null +++ b/logical/plugin/mock/path_internal.go @@ -0,0 +1,28 @@ +package mock + +import ( + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathInternal(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "internal", + Fields: map[string]*framework.FieldSchema{}, + ExistenceCheck: b.pathTestingExistenceCheck, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathTestingReadInternal, + }, + } +} + +func (b *backend) pathTestingReadInternal( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + // Return the secret + return &logical.Response{ + Data: map[string]interface{}{ + "value": b.internal, + }, + }, nil + +} diff --git a/logical/plugin/mock/path_testing.go b/logical/plugin/mock/path_testing.go new file mode 100644 index 0000000000..b5374606a0 --- /dev/null +++ b/logical/plugin/mock/path_testing.go @@ -0,0 +1,56 @@ +package mock + +import ( + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathTesting(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "test/ing", + Fields: map[string]*framework.FieldSchema{ + "value": &framework.FieldSchema{Type: framework.TypeString}, + }, + ExistenceCheck: b.pathTestingExistenceCheck, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathTestingRead, + logical.CreateOperation: b.pathTestingCreate, + }, + } +} + +func (b *backend) pathTestingRead( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + // Return the secret + return &logical.Response{ + Data: map[string]interface{}{ + "value": data.Get("value"), + }, + }, nil +} + +func (b *backend) pathTestingCreate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + val := data.Get("value").(string) + + entry := &logical.StorageEntry{ + Key: "test/ing", + Value: []byte(val), + } + + s := req.Storage + err := s.Put(entry) + if err != nil { + return nil, err + } + + return &logical.Response{ + Data: map[string]interface{}{ + "value": data.Get("value"), + }, + }, nil +} + +func (b *backend) pathTestingExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { + return false, nil +} diff --git a/logical/plugin/plugin.go b/logical/plugin/plugin.go new file mode 100644 index 0000000000..7736943f6c --- /dev/null +++ b/logical/plugin/plugin.go @@ -0,0 +1,96 @@ +package plugin + +import ( + "fmt" + + "sync" + + "github.com/hashicorp/go-plugin" + "github.com/hashicorp/vault/helper/pluginutil" + "github.com/hashicorp/vault/logical" +) + +// BackendPluginClient is a wrapper around backendPluginClient +// that also contains its plugin.Client instance. It's primarily +// used to cleanly kill the client on Cleanup() +type BackendPluginClient struct { + client *plugin.Client + sync.Mutex + + *backendPluginClient +} + +// Cleanup calls the RPC client's Cleanup() func and also calls +// the go-plugin's client Kill() func +func (b *BackendPluginClient) Cleanup() { + b.backendPluginClient.Cleanup() + b.client.Kill() +} + +// NewBackend will return an instance of an RPC-based client implementation of the backend for +// external plugins, or a concrete implementation of the backend if it is a builtin backend. +// The backend is returned as a logical.Backend interface. +func NewBackend(pluginName string, sys pluginutil.LookRunnerUtil) (logical.Backend, error) { + // Look for plugin in the plugin catalog + pluginRunner, err := sys.LookupPlugin(pluginName) + if err != nil { + return nil, err + } + + var backend logical.Backend + if pluginRunner.Builtin { + // Plugin is builtin so we can retrieve an instance of the interface + // from the pluginRunner. Then cast it to logical.Backend. + backendRaw, err := pluginRunner.BuiltinFactory() + if err != nil { + return nil, fmt.Errorf("error getting plugin type: %s", err) + } + + var ok bool + backend, ok = backendRaw.(logical.Backend) + if !ok { + return nil, fmt.Errorf("unsuported backend type: %s", pluginName) + } + + } else { + // create a backendPluginClient instance + backend, err = newPluginClient(sys, pluginRunner) + if err != nil { + return nil, err + } + } + + return backend, nil +} + +func newPluginClient(sys pluginutil.RunnerUtil, pluginRunner *pluginutil.PluginRunner) (logical.Backend, error) { + // pluginMap is the map of plugins we can dispense. + pluginMap := map[string]plugin.Plugin{ + "backend": &BackendPlugin{}, + } + client, err := pluginRunner.Run(sys, pluginMap, handshakeConfig, []string{}) + if err != nil { + return nil, err + } + + // Connect via RPC + rpcClient, err := client.Client() + if err != nil { + return nil, err + } + + // Request the plugin + raw, err := rpcClient.Dispense("backend") + if err != nil { + return nil, err + } + + // We should have a logical backend type now. This feels like a normal interface + // implementation but is in fact over an RPC connection. + backendRPC := raw.(*backendPluginClient) + + return &BackendPluginClient{ + client: client, + backendPluginClient: backendRPC, + }, nil +} diff --git a/logical/plugin/serve.go b/logical/plugin/serve.go new file mode 100644 index 0000000000..4eb69a8c54 --- /dev/null +++ b/logical/plugin/serve.go @@ -0,0 +1,54 @@ +package plugin + +import ( + "crypto/tls" + + "github.com/hashicorp/go-plugin" + "github.com/hashicorp/vault/helper/pluginutil" + "github.com/hashicorp/vault/logical" +) + +// BackendPluginName is the name of the plugin that can be +// dispensed rom the plugin server. +const BackendPluginName = "backend" + +type BackendFactoryFunc func(*logical.BackendConfig) (logical.Backend, error) +type TLSProdiverFunc func() (*tls.Config, error) + +type ServeOpts struct { + BackendFactoryFunc BackendFactoryFunc + TLSProviderFunc TLSProdiverFunc +} + +// Serve is used to serve a backend plugin +func Serve(opts *ServeOpts) error { + // pluginMap is the map of plugins we can dispense. + var pluginMap = map[string]plugin.Plugin{ + "backend": &BackendPlugin{ + Factory: opts.BackendFactoryFunc, + }, + } + + err := pluginutil.OptionallyEnableMlock() + if err != nil { + return err + } + + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: handshakeConfig, + Plugins: pluginMap, + TLSProvider: opts.TLSProviderFunc, + }) + + return nil +} + +// handshakeConfigs are used to just do a basic handshake between +// a plugin and host. If the handshake fails, a user friendly error is shown. +// This prevents users from executing bad plugins or executing a plugin +// directory. It is a UX feature, not a security feature. +var handshakeConfig = plugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "VAULT_BACKEND_PLUGIN", + MagicCookieValue: "6669da05-b1c8-4f49-97d9-c8e5bed98e20", +} diff --git a/logical/plugin/storage.go b/logical/plugin/storage.go new file mode 100644 index 0000000000..55cea8449b --- /dev/null +++ b/logical/plugin/storage.go @@ -0,0 +1,119 @@ +package plugin + +import ( + "net/rpc" + + "github.com/hashicorp/go-plugin" + "github.com/hashicorp/vault/logical" +) + +// StorageClient is an implementation of logical.Storage that communicates +// over RPC. +type StorageClient struct { + client *rpc.Client +} + +func (s *StorageClient) List(prefix string) ([]string, error) { + var reply StorageListReply + err := s.client.Call("Plugin.List", prefix, &reply) + if err != nil { + return reply.Keys, err + } + if reply.Error != nil { + return reply.Keys, reply.Error + } + return reply.Keys, nil +} + +func (s *StorageClient) Get(key string) (*logical.StorageEntry, error) { + var reply StorageGetReply + err := s.client.Call("Plugin.Get", key, &reply) + if err != nil { + return nil, err + } + if reply.Error != nil { + return nil, reply.Error + } + return reply.StorageEntry, nil +} + +func (s *StorageClient) Put(entry *logical.StorageEntry) error { + var reply StoragePutReply + err := s.client.Call("Plugin.Put", entry, &reply) + if err != nil { + return err + } + if reply.Error != nil { + return reply.Error + } + return nil +} + +func (s *StorageClient) Delete(key string) error { + var reply StorageDeleteReply + err := s.client.Call("Plugin.Delete", key, &reply) + if err != nil { + return err + } + if reply.Error != nil { + return reply.Error + } + return nil +} + +// StorageServer is a net/rpc compatible structure for serving +type StorageServer struct { + impl logical.Storage +} + +func (s *StorageServer) List(prefix string, reply *StorageListReply) error { + keys, err := s.impl.List(prefix) + *reply = StorageListReply{ + Keys: keys, + Error: plugin.NewBasicError(err), + } + return nil +} + +func (s *StorageServer) Get(key string, reply *StorageGetReply) error { + storageEntry, err := s.impl.Get(key) + *reply = StorageGetReply{ + StorageEntry: storageEntry, + Error: plugin.NewBasicError(err), + } + return nil +} + +func (s *StorageServer) Put(entry *logical.StorageEntry, reply *StoragePutReply) error { + err := s.impl.Put(entry) + *reply = StoragePutReply{ + Error: plugin.NewBasicError(err), + } + return nil +} + +func (s *StorageServer) Delete(key string, reply *StorageDeleteReply) error { + err := s.impl.Delete(key) + *reply = StorageDeleteReply{ + Error: plugin.NewBasicError(err), + } + return nil +} + +type StorageListReply struct { + Keys []string + Error *plugin.BasicError +} + +type StorageGetReply struct { + StorageEntry *logical.StorageEntry + Error *plugin.BasicError +} + +type StoragePutReply struct { + Error *plugin.BasicError +} + +type StorageDeleteReply struct { + Error *plugin.BasicError +} diff --git a/logical/plugin/storage_test.go b/logical/plugin/storage_test.go new file mode 100644 index 0000000000..9899a82be2 --- /dev/null +++ b/logical/plugin/storage_test.go @@ -0,0 +1,27 @@ +package plugin + +import ( + "testing" + + plugin "github.com/hashicorp/go-plugin" + "github.com/hashicorp/vault/logical" +) + +func TestStorage_impl(t *testing.T) { + var _ logical.Storage = new(StorageClient) +} + +func TestStorage_operations(t *testing.T) { + client, server := plugin.TestRPCConn(t) + defer client.Close() + + storage := &logical.InmemStorage{} + + server.RegisterName("Plugin", &StorageServer{ + impl: storage, + }) + + testStorage := &StorageClient{client: client} + + logical.TestStorage(t, testStorage) +} diff --git a/logical/plugin/system.go b/logical/plugin/system.go new file mode 100644 index 0000000000..16f67df19e --- /dev/null +++ b/logical/plugin/system.go @@ -0,0 +1,247 @@ +package plugin + +import ( + "net/rpc" + "time" + + "fmt" + + plugin "github.com/hashicorp/go-plugin" + "github.com/hashicorp/vault/helper/consts" + "github.com/hashicorp/vault/helper/pluginutil" + "github.com/hashicorp/vault/helper/wrapping" + "github.com/hashicorp/vault/logical" +) + +type SystemViewClient struct { + client *rpc.Client +} + +func (s *SystemViewClient) DefaultLeaseTTL() time.Duration { + var reply DefaultLeaseTTLReply + err := s.client.Call("Plugin.DefaultLeaseTTL", new(interface{}), &reply) + if err != nil { + return 0 + } + + return reply.DefaultLeaseTTL +} + +func (s *SystemViewClient) MaxLeaseTTL() time.Duration { + var reply MaxLeaseTTLReply + err := s.client.Call("Plugin.MaxLeaseTTL", new(interface{}), &reply) + if err != nil { + return 0 + } + + return reply.MaxLeaseTTL +} + +func (s *SystemViewClient) SudoPrivilege(path string, token string) bool { + var reply SudoPrivilegeReply + args := &SudoPrivilegeArgs{ + Path: path, + Token: token, + } + + err := s.client.Call("Plugin.SudoPrivilege", args, &reply) + if err != nil { + return false + } + + return reply.Sudo +} + +func (s *SystemViewClient) Tainted() bool { + var reply TaintedReply + + err := s.client.Call("Plugin.Tainted", new(interface{}), &reply) + if err != nil { + return false + } + + return reply.Tainted +} + +func (s *SystemViewClient) CachingDisabled() bool { + var reply CachingDisabledReply + + err := s.client.Call("Plugin.CachingDisabled", new(interface{}), &reply) + if err != nil { + return false + } + + return reply.CachingDisabled +} + +func (s *SystemViewClient) ReplicationState() consts.ReplicationState { + var reply ReplicationStateReply + + err := s.client.Call("Plugin.ReplicationState", new(interface{}), &reply) + if err != nil { + return consts.ReplicationDisabled + } + + return reply.ReplicationState +} + +func (s *SystemViewClient) ResponseWrapData(data map[string]interface{}, ttl time.Duration, jwt bool) (*wrapping.ResponseWrapInfo, error) { + var reply ResponseWrapDataReply + // Do not allow JWTs to be returned + args := &ResponseWrapDataArgs{ + Data: data, + TTL: ttl, + JWT: false, + } + + err := s.client.Call("Plugin.ResponseWrapData", args, &reply) + if err != nil { + return nil, err + } + if reply.Error != nil { + return nil, reply.Error + } + + return reply.ResponseWrapInfo, nil +} + +func (s *SystemViewClient) LookupPlugin(name string) (*pluginutil.PluginRunner, error) { + return nil, fmt.Errorf("cannot call LookupPlugin from a plugin backend") +} + +func (s *SystemViewClient) MlockEnabled() bool { + var reply MlockEnabledReply + err := s.client.Call("Plugin.MlockEnabled", new(interface{}), &reply) + if err != nil { + return false + } + + return reply.MlockEnabled +} + +type SystemViewServer struct { + impl logical.SystemView +} + +func (s *SystemViewServer) DefaultLeaseTTL(_ interface{}, reply *DefaultLeaseTTLReply) error { + ttl := s.impl.DefaultLeaseTTL() + *reply = DefaultLeaseTTLReply{ + DefaultLeaseTTL: ttl, + } + + return nil +} + +func (s *SystemViewServer) MaxLeaseTTL(_ interface{}, reply *MaxLeaseTTLReply) error { + ttl := s.impl.MaxLeaseTTL() + *reply = MaxLeaseTTLReply{ + MaxLeaseTTL: ttl, + } + + return nil +} + +func (s *SystemViewServer) SudoPrivilege(args *SudoPrivilegeArgs, reply *SudoPrivilegeReply) error { + sudo := s.impl.SudoPrivilege(args.Path, args.Token) + *reply = SudoPrivilegeReply{ + Sudo: sudo, + } + + return nil +} + +func (s *SystemViewServer) Tainted(_ interface{}, reply *TaintedReply) error { + tainted := s.impl.Tainted() + *reply = TaintedReply{ + Tainted: tainted, + } + + return nil +} + +func (s *SystemViewServer) CachingDisabled(_ interface{}, reply *CachingDisabledReply) error { + cachingDisabled := s.impl.CachingDisabled() + *reply = CachingDisabledReply{ + CachingDisabled: cachingDisabled, + } + + return nil +} + +func (s *SystemViewServer) ReplicationState(_ interface{}, reply *ReplicationStateReply) error { + replicationState := s.impl.ReplicationState() + *reply = ReplicationStateReply{ + ReplicationState: replicationState, + } + + return nil +} + +func (s *SystemViewServer) ResponseWrapData(args *ResponseWrapDataArgs, reply *ResponseWrapDataReply) error { + // Do not allow JWTs to be returned + info, err := s.impl.ResponseWrapData(args.Data, args.TTL, false) + if err != nil { + *reply = ResponseWrapDataReply{ + Error: plugin.NewBasicError(err), + } + return nil + } + *reply = ResponseWrapDataReply{ + ResponseWrapInfo: info, + } + + return nil +} + +func (s *SystemViewServer) MlockEnabled(_ interface{}, reply *MlockEnabledReply) error { + enabled := s.impl.MlockEnabled() + *reply = MlockEnabledReply{ + MlockEnabled: enabled, + } + + return nil +} + +type DefaultLeaseTTLReply struct { + DefaultLeaseTTL time.Duration +} + +type MaxLeaseTTLReply struct { + MaxLeaseTTL time.Duration +} + +type SudoPrivilegeArgs struct { + Path string + Token string +} + +type SudoPrivilegeReply struct { + Sudo bool +} + +type TaintedReply struct { + Tainted bool +} + +type CachingDisabledReply struct { + CachingDisabled bool +} + +type ReplicationStateReply struct { + ReplicationState consts.ReplicationState +} + +type ResponseWrapDataArgs struct { + Data map[string]interface{} + TTL time.Duration + JWT bool +} + +type ResponseWrapDataReply struct { + ResponseWrapInfo *wrapping.ResponseWrapInfo + Error *plugin.BasicError +} + +type MlockEnabledReply struct { + MlockEnabled bool +} diff --git a/logical/plugin/system_test.go b/logical/plugin/system_test.go new file mode 100644 index 0000000000..2fd82dcdca --- /dev/null +++ b/logical/plugin/system_test.go @@ -0,0 +1,174 @@ +package plugin + +import ( + "testing" + + "reflect" + + plugin "github.com/hashicorp/go-plugin" + "github.com/hashicorp/vault/helper/consts" + "github.com/hashicorp/vault/logical" +) + +func Test_impl(t *testing.T) { + var _ logical.SystemView = new(SystemViewClient) +} + +func TestSystem_defaultLeaseTTL(t *testing.T) { + client, server := plugin.TestRPCConn(t) + defer client.Close() + + sys := logical.TestSystemView() + + server.RegisterName("Plugin", &SystemViewServer{ + impl: sys, + }) + + testSystemView := &SystemViewClient{client: client} + + expected := sys.DefaultLeaseTTL() + actual := testSystemView.DefaultLeaseTTL() + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("expected: %v, got: %v", expected, actual) + } +} + +func TestSystem_maxLeaseTTL(t *testing.T) { + client, server := plugin.TestRPCConn(t) + defer client.Close() + + sys := logical.TestSystemView() + + server.RegisterName("Plugin", &SystemViewServer{ + impl: sys, + }) + + testSystemView := &SystemViewClient{client: client} + + expected := sys.MaxLeaseTTL() + actual := testSystemView.MaxLeaseTTL() + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("expected: %v, got: %v", expected, actual) + } +} + +func TestSystem_sudoPrivilege(t *testing.T) { + client, server := plugin.TestRPCConn(t) + defer client.Close() + + sys := logical.TestSystemView() + sys.SudoPrivilegeVal = true + + server.RegisterName("Plugin", &SystemViewServer{ + impl: sys, + }) + + testSystemView := &SystemViewClient{client: client} + + expected := sys.SudoPrivilege("foo", "bar") + actual := testSystemView.SudoPrivilege("foo", "bar") + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("expected: %v, got: %v", expected, actual) + } +} + +func TestSystem_tainted(t *testing.T) { + client, server := plugin.TestRPCConn(t) + defer client.Close() + + sys := logical.TestSystemView() + sys.TaintedVal = true + + server.RegisterName("Plugin", &SystemViewServer{ + impl: sys, + }) + + testSystemView := &SystemViewClient{client: client} + + expected := sys.Tainted() + actual := testSystemView.Tainted() + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("expected: %v, got: %v", expected, actual) + } +} + +func TestSystem_cachingDisabled(t *testing.T) { + client, server := plugin.TestRPCConn(t) + defer client.Close() + + sys := logical.TestSystemView() + sys.CachingDisabledVal = true + + server.RegisterName("Plugin", &SystemViewServer{ + impl: sys, + }) + + testSystemView := &SystemViewClient{client: client} + + expected := sys.CachingDisabled() + actual := testSystemView.CachingDisabled() + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("expected: %v, got: %v", expected, actual) + } +} + +func TestSystem_replicationState(t *testing.T) { + client, server := plugin.TestRPCConn(t) + defer client.Close() + + sys := logical.TestSystemView() + sys.ReplicationStateVal = consts.ReplicationPrimary + + server.RegisterName("Plugin", &SystemViewServer{ + impl: sys, + }) + + testSystemView := &SystemViewClient{client: client} + + expected := sys.ReplicationState() + actual := testSystemView.ReplicationState() + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("expected: %v, got: %v", expected, actual) + } +} + +func TestSystem_responseWrapData(t *testing.T) { + t.SkipNow() +} + +func TestSystem_lookupPlugin(t *testing.T) { + client, server := plugin.TestRPCConn(t) + defer client.Close() + + sys := logical.TestSystemView() + + server.RegisterName("Plugin", &SystemViewServer{ + impl: sys, + }) + + testSystemView := &SystemViewClient{client: client} + + if _, err := testSystemView.LookupPlugin("foo"); err == nil { + t.Fatal("LookPlugin(): expected error on due to unsupported call from plugin") + } +} + +func TestSystem_mlockEnabled(t *testing.T) { + client, server := plugin.TestRPCConn(t) + defer client.Close() + + sys := logical.TestSystemView() + sys.EnableMlock = true + + server.RegisterName("Plugin", &SystemViewServer{ + impl: sys, + }) + + testSystemView := &SystemViewClient{client: client} + + expected := sys.MlockEnabled() + actual := testSystemView.MlockEnabled() + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("expected: %v, got: %v", expected, actual) + } +} diff --git a/vault/auth.go b/vault/auth.go index 79b300980f..608a217a54 100644 --- a/vault/auth.go +++ b/vault/auth.go @@ -95,9 +95,13 @@ func (c *Core) enableCredential(entry *MountEntry) error { viewPath := credentialBarrierPrefix + entry.UUID + "/" view := NewBarrierView(c.barrier, viewPath) sysView := c.mountEntrySysView(entry) + conf := make(map[string]string) + if entry.Config.PluginName != "" { + conf["plugin_name"] = entry.Config.PluginName + } // Create the new backend - backend, err := c.newCredentialBackend(entry.Type, sysView, view, nil) + backend, err := c.newCredentialBackend(entry.Type, sysView, view, conf) if err != nil { return err } @@ -105,6 +109,12 @@ func (c *Core) enableCredential(entry *MountEntry) error { return fmt.Errorf("nil backend returned from %q factory", entry.Type) } + // Check for the correct backend type + backendType := backend.Type() + if entry.Type == "plugin" && backendType != logical.TypeCredential { + return fmt.Errorf("cannot mount '%s' of type '%s' as an auth backend", entry.Config.PluginName, backendType) + } + if err := backend.Initialize(); err != nil { return err } @@ -406,9 +416,13 @@ func (c *Core) setupCredentials() error { viewPath := credentialBarrierPrefix + entry.UUID + "/" view = NewBarrierView(c.barrier, viewPath) sysView := c.mountEntrySysView(entry) + conf := make(map[string]string) + if entry.Config.PluginName != "" { + conf["plugin_name"] = entry.Config.PluginName + } // Initialize the backend - backend, err = c.newCredentialBackend(entry.Type, sysView, view, nil) + backend, err = c.newCredentialBackend(entry.Type, sysView, view, conf) if err != nil { c.logger.Error("core: failed to create credential entry", "path", entry.Path, "error", err) return errLoadAuthFailed @@ -417,6 +431,12 @@ func (c *Core) setupCredentials() error { return fmt.Errorf("nil backend returned from %q factory", entry.Type) } + // Check for the correct backend type + backendType := backend.Type() + if entry.Type == "plugin" && backendType != logical.TypeCredential { + return fmt.Errorf("cannot mount '%s' of type '%s' as an auth backend", entry.Config.PluginName, backendType) + } + if err := backend.Initialize(); err != nil { return err } diff --git a/vault/core.go b/vault/core.go index 725f7f775e..2670c2e55d 100644 --- a/vault/core.go +++ b/vault/core.go @@ -516,7 +516,10 @@ func NewCore(conf *CoreConfig) (*Core, error) { logicalBackends["cubbyhole"] = CubbyholeBackendFactory logicalBackends["system"] = func(config *logical.BackendConfig) (logical.Backend, error) { b := NewSystemBackend(c) - return b.Backend.Setup(config) + if err := b.Setup(config); err != nil { + return nil, err + } + return b, nil } c.logicalBackends = logicalBackends diff --git a/vault/expiration_test.go b/vault/expiration_test.go index 0b9812b2d3..ad50adf68a 100644 --- a/vault/expiration_test.go +++ b/vault/expiration_test.go @@ -1459,5 +1459,10 @@ func badRenewFactory(conf *logical.BackendConfig) (logical.Backend, error) { }, } - return be.Setup(conf) + err := be.Setup(conf) + if err != nil { + return nil, err + } + + return be, nil } diff --git a/vault/logical_passthrough.go b/vault/logical_passthrough.go index 463884caf0..2b43379882 100644 --- a/vault/logical_passthrough.go +++ b/vault/logical_passthrough.go @@ -17,13 +17,13 @@ func PassthroughBackendFactory(conf *logical.BackendConfig) (logical.Backend, er return LeaseSwitchedPassthroughBackend(conf, false) } -// PassthroughBackendWithLeasesFactory returns a PassthroughBackend +// LeasedPassthroughBackendFactory returns a PassthroughBackend // with leases switched on func LeasedPassthroughBackendFactory(conf *logical.BackendConfig) (logical.Backend, error) { return LeaseSwitchedPassthroughBackend(conf, true) } -// LeaseSwitchedPassthroughBackendFactory returns a PassthroughBackend +// LeaseSwitchedPassthroughBackend returns a PassthroughBackend // with leases switched on or off func LeaseSwitchedPassthroughBackend(conf *logical.BackendConfig, leases bool) (logical.Backend, error) { var b PassthroughBackend @@ -147,6 +147,10 @@ func (b *PassthroughBackend) handleRead( return resp, nil } +func (b *PassthroughBackend) GeneratesLeases() bool { + return b.generateLeases +} + func (b *PassthroughBackend) handleWrite( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { // Check that some fields are given @@ -202,10 +206,6 @@ func (b *PassthroughBackend) handleList( return logical.ListResponse(keys), nil } -func (b *PassthroughBackend) GeneratesLeases() bool { - return b.generateLeases -} - const passthroughHelp = ` The generic backend reads and writes arbitrary secrets to the backend. The secrets are encrypted/decrypted by Vault: they are never stored diff --git a/vault/logical_system.go b/vault/logical_system.go index 583859d05a..97814931e4 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -9,6 +9,7 @@ import ( "sync" "time" + "github.com/fatih/structs" "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/parseutil" "github.com/hashicorp/vault/helper/wrapping" @@ -486,6 +487,10 @@ func NewSystemBackend(core *Core) *SystemBackend { Type: framework.TypeString, Description: strings.TrimSpace(sysHelp["auth_desc"][0]), }, + "plugin_name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["auth_plugin"][0]), + }, "local": &framework.FieldSchema{ Type: framework.TypeBool, Default: false, @@ -775,7 +780,7 @@ func NewSystemBackend(core *Core) *SystemBackend { HelpDescription: strings.TrimSpace(sysHelp["audited-headers"][1]), }, &framework.Path{ - Pattern: "plugins/catalog/$", + Pattern: "plugins/catalog/?$", Fields: map[string]*framework.FieldSchema{}, @@ -792,18 +797,15 @@ func NewSystemBackend(core *Core) *SystemBackend { Fields: map[string]*framework.FieldSchema{ "name": &framework.FieldSchema{ Type: framework.TypeString, - Description: "The name of the plugin", + Description: strings.TrimSpace(sysHelp["plugin-catalog_name"][0]), }, "sha_256": &framework.FieldSchema{ - Type: framework.TypeString, - Description: `The SHA256 sum of the executable used in the - command field. This should be HEX encoded.`, + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["plugin-catalog_sha-256"][0]), }, "command": &framework.FieldSchema{ - Type: framework.TypeString, - Description: `The command used to start the plugin. The - executable defined in this command must exist in vault's - plugin directory.`, + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["plugin-catalog_command"][0]), }, }, @@ -943,10 +945,11 @@ func (b *SystemBackend) handlePluginCatalogRead(req *logical.Request, d *framewo return nil, nil } + // Create a map of data to be returned and remove sensitive information from it + data := structs.New(plugin).Map() + return &logical.Response{ - Data: map[string]interface{}{ - "plugin": plugin, - }, + Data: data, }, nil } @@ -1157,18 +1160,17 @@ func (b *SystemBackend) handleMountTable( } for _, entry := range b.Core.mounts.Entries { + // Populate mount info + structConfig := structs.New(entry.Config).Map() + structConfig["default_lease_ttl"] = int64(structConfig["default_lease_ttl"].(time.Duration).Seconds()) + structConfig["max_lease_ttl"] = int64(structConfig["max_lease_ttl"].(time.Duration).Seconds()) info := map[string]interface{}{ "type": entry.Type, "description": entry.Description, "accessor": entry.Accessor, - "config": map[string]interface{}{ - "default_lease_ttl": int64(entry.Config.DefaultLeaseTTL.Seconds()), - "max_lease_ttl": int64(entry.Config.MaxLeaseTTL.Seconds()), - "force_no_cache": entry.Config.ForceNoCache, - }, - "local": entry.Local, + "config": structConfig, + "local": entry.Local, } - resp.Data[entry.Path] = info } @@ -1195,12 +1197,8 @@ func (b *SystemBackend) handleMount( path = sanitizeMountPath(path) var config MountConfig + var apiConfig APIMountConfig - var apiConfig struct { - DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` - MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` - ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` - } configMap := data.Get("config").(map[string]interface{}) if configMap != nil && len(configMap) != 0 { err := mapstructure.Decode(configMap, &apiConfig) @@ -1249,6 +1247,11 @@ func (b *SystemBackend) handleMount( logical.ErrInvalidRequest } + // Only set plugin-name if mount is of type plugin + if logicalType == "plugin" && apiConfig.PluginName != "" { + config.PluginName = apiConfig.PluginName + } + // Copy over the force no cache if set if apiConfig.ForceNoCache { config.ForceNoCache = true @@ -1685,6 +1688,14 @@ func (b *SystemBackend) handleEnableAuth( path := data.Get("path").(string) logicalType := data.Get("type").(string) description := data.Get("description").(string) + pluginName := data.Get("plugin_name").(string) + + var config MountConfig + + // Only set plugin name if mount is of type plugin + if logicalType == "plugin" && pluginName != "" { + config.PluginName = pluginName + } if logicalType == "" { return logical.ErrorResponse( @@ -1700,6 +1711,7 @@ func (b *SystemBackend) handleEnableAuth( Path: path, Type: logicalType, Description: description, + Config: config, Local: local, } @@ -2585,6 +2597,11 @@ Example: you might have an OAuth backend for GitHub, and one for Google Apps. "", }, + "auth_plugin": { + `Name of the auth plugin to use based from the name in the plugin catalog.`, + "", + }, + "policy-list": { `List the configured access control policies.`, ` @@ -2764,23 +2781,38 @@ This path responds to the following HTTP methods. "Lists the headers configured to be audited.", `Returns a list of headers that have been configured to be audited.`, }, - "plugins/catalog": { - `Configures the plugins known to vault`, + "plugin-catalog": { + "Configures the plugins known to vault", ` This path responds to the following HTTP methods. - LIST / - Returns a list of names of configured plugins. + LIST / + Returns a list of names of configured plugins. - GET / - Retrieve the metadata for the named plugin. + GET / + Retrieve the metadata for the named plugin. - PUT / - Add or update plugin. + PUT / + Add or update plugin. - DELETE / - Delete the plugin with the given name. + DELETE / + Delete the plugin with the given name. `, }, + "plugin-catalog_name": { + "The name of the plugin", + "", + }, + "plugin-catalog_sha-256": { + `The SHA256 sum of the executable used in the +command field. This should be HEX encoded.`, + "", + }, + "plugin-catalog_command": { + `The command used to start the plugin. The +executable defined in this command must exist in vault's +plugin directory.`, + "", + }, "leases": { `View or list lease metadata.`, ` diff --git a/vault/logical_system_integ_test.go b/vault/logical_system_integ_test.go new file mode 100644 index 0000000000..541304140f --- /dev/null +++ b/vault/logical_system_integ_test.go @@ -0,0 +1,104 @@ +package vault_test + +import ( + "io/ioutil" + "os" + "testing" + "time" + + "github.com/hashicorp/vault/builtin/plugin" + "github.com/hashicorp/vault/helper/logformat" + "github.com/hashicorp/vault/helper/pluginutil" + "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/logical" + lplugin "github.com/hashicorp/vault/logical/plugin" + "github.com/hashicorp/vault/logical/plugin/mock" + "github.com/hashicorp/vault/vault" + log "github.com/mgutz/logxi/v1" +) + +func TestSystemBackend_enableAuth_plugin(t *testing.T) { + coreConfig := &vault.CoreConfig{ + CredentialBackends: map[string]logical.Factory{ + "plugin": plugin.Factory, + }, + } + + cluster := vault.NewTestCluster(t, coreConfig, true) + cluster.StartListeners() + defer cluster.CloseListeners() + cores := cluster.Cores + + cores[0].Handler.Handle("/", http.Handler(cores[0].Core)) + cores[1].Handler.Handle("/", http.Handler(cores[1].Core)) + cores[2].Handler.Handle("/", http.Handler(cores[2].Core)) + + core := cores[0] + + b := vault.NewSystemBackend(core.Core) + logger := logformat.NewVaultLogger(log.LevelTrace) + bc := &logical.BackendConfig{ + Logger: logger, + System: logical.StaticSystemView{ + DefaultLeaseTTLVal: time.Hour * 24, + MaxLeaseTTLVal: time.Hour * 24 * 32, + }, + } + + err := b.Backend.Setup(bc) + if err != nil { + t.Fatal(err) + } + + vault.TestAddTestPlugin(t, core.Core, "mock-plugin", "TestBackend_PluginMain") + + req := logical.TestRequest(t, logical.UpdateOperation, "auth/mock-plugin") + req.Data["type"] = "plugin" + req.Data["plugin_name"] = "mock-plugin" + + resp, err := b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %v", resp) + } +} + +func TestBackend_PluginMain(t *testing.T) { + if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" { + return + } + + content := []byte(vault.TestClusterCACert) + tmpfile, err := ioutil.TempFile("", "test-cacert") + if err != nil { + t.Fatal(err) + } + + defer os.Remove(tmpfile.Name()) // clean up + + if _, err := tmpfile.Write(content); err != nil { + t.Fatal(err) + } + if err := tmpfile.Close(); err != nil { + t.Fatal(err) + } + + factoryFunc := mock.FactoryType(logical.TypeCredential) + + args := []string{"--ca-cert=" + tmpfile.Name()} + + apiClientMeta := &pluginutil.APIClientMeta{} + flags := apiClientMeta.FlagSet() + flags.Parse(args) + tlsConfig := apiClientMeta.GetTLSConfig() + tlsProviderFunc := pluginutil.VaultPluginTLSProvider(tlsConfig) + err = lplugin.Serve(&lplugin.ServeOpts{ + BackendFactoryFunc: factoryFunc, + TLSProviderFunc: tlsProviderFunc, + }) + if err != nil { + t.Fatal(err) + } +} diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 12b7c5de3f..4fa563d25d 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -985,8 +985,8 @@ func TestSystemBackend_revokePrefixAuth(t *testing.T) { MaxLeaseTTLVal: time.Hour * 24 * 32, }, } - be := NewSystemBackend(core) - b, err := be.Backend.Setup(bc) + b := NewSystemBackend(core) + err := b.Backend.Setup(bc) if err != nil { t.Fatal(err) } @@ -1049,8 +1049,8 @@ func TestSystemBackend_revokePrefixAuth_origUrl(t *testing.T) { MaxLeaseTTLVal: time.Hour * 24 * 32, }, } - be := NewSystemBackend(core) - b, err := be.Backend.Setup(bc) + b := NewSystemBackend(core) + err := b.Backend.Setup(bc) if err != nil { t.Fatal(err) } @@ -1591,7 +1591,7 @@ func testSystemBackend(t *testing.T) logical.Backend { } b := NewSystemBackend(c) - _, err := b.Backend.Setup(bc) + err := b.Backend.Setup(bc) if err != nil { t.Fatal(err) } @@ -1610,7 +1610,7 @@ func testCoreSystemBackend(t *testing.T) (*Core, logical.Backend, string) { } b := NewSystemBackend(c) - _, err := b.Backend.Setup(bc) + err := b.Backend.Setup(bc) if err != nil { t.Fatal(err) } @@ -1641,22 +1641,16 @@ func TestSystemBackend_PluginCatalog_CRUD(t *testing.T) { if err != nil { t.Fatalf("err: %v", err) } + actualRespData := resp.Data expectedBuiltin := &pluginutil.PluginRunner{ Name: "mysql-database-plugin", Builtin: true, } - expectedBuiltin.BuiltinFactory, _ = builtinplugins.Get("mysql-database-plugin") + expectedRespData := structs.New(expectedBuiltin).Map() - p := resp.Data["plugin"].(*pluginutil.PluginRunner) - if &(p.BuiltinFactory) == &(expectedBuiltin.BuiltinFactory) { - t.Fatal("expected BuiltinFactory did not match actual") - } - - expectedBuiltin.BuiltinFactory = nil - p.BuiltinFactory = nil - if !reflect.DeepEqual(p, expectedBuiltin) { - t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", resp.Data["plugin"].(*pluginutil.PluginRunner), expectedBuiltin) + if !reflect.DeepEqual(actualRespData, expectedRespData) { + t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", actualRespData, expectedRespData) } // Set a plugin @@ -1680,16 +1674,19 @@ func TestSystemBackend_PluginCatalog_CRUD(t *testing.T) { if err != nil { t.Fatalf("err: %v", err) } + actual := resp.Data - expected := &pluginutil.PluginRunner{ + expectedRunner := &pluginutil.PluginRunner{ Name: "test-plugin", Command: filepath.Join(sym, filepath.Base(file.Name())), Args: []string{"--test"}, Sha256: []byte{'1'}, Builtin: false, } - if !reflect.DeepEqual(resp.Data["plugin"].(*pluginutil.PluginRunner), expected) { - t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", resp.Data["plugin"].(*pluginutil.PluginRunner), expected) + expected := structs.New(expectedRunner).Map() + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", actual, expected) } // Delete plugin diff --git a/vault/mount.go b/vault/mount.go index 4757548f48..84eef5f8ed 100644 --- a/vault/mount.go +++ b/vault/mount.go @@ -167,6 +167,15 @@ type MountConfig struct { DefaultLeaseTTL time.Duration `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` // Override for global default MaxLeaseTTL time.Duration `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` // Override for global default ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` // Override for global default + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` +} + +// APIMountConfig is an embedded struct of api.MountConfigInput +type APIMountConfig struct { + DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` + MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` + ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` + PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"` } // Mount is used to mount a new backend to the mount table. @@ -216,8 +225,13 @@ func (c *Core) mount(entry *MountEntry) error { viewPath := backendBarrierPrefix + entry.UUID + "/" view := NewBarrierView(c.barrier, viewPath) sysView := c.mountEntrySysView(entry) + conf := make(map[string]string) + if entry.Config.PluginName != "" { + conf["plugin_name"] = entry.Config.PluginName + } - backend, err := c.newLogicalBackend(entry.Type, sysView, view, nil) + // Consider having plugin name under entry.Options + backend, err := c.newLogicalBackend(entry.Type, sysView, view, conf) if err != nil { return err } @@ -225,8 +239,14 @@ func (c *Core) mount(entry *MountEntry) error { return fmt.Errorf("nil backend of type %q returned from creation function", entry.Type) } + // Check for the correct backend type + backendType := backend.Type() + if entry.Type == "plugin" && backendType != logical.TypeLogical { + return fmt.Errorf("cannot mount '%s' of type '%s' as a logical backend", entry.Config.PluginName, backendType) + } + // Call initialize; this takes care of init tasks that must be run after - // the ignore paths are collected + // the ignore paths are collected. if err := backend.Initialize(); err != nil { return err } @@ -658,9 +678,13 @@ func (c *Core) setupMounts() error { // Create a barrier view using the UUID view = NewBarrierView(c.barrier, barrierPath) sysView := c.mountEntrySysView(entry) - // Initialize the backend + // Set up conf to pass in plugin_name + conf := make(map[string]string) + if entry.Config.PluginName != "" { + conf["plugin_name"] = entry.Config.PluginName + } // Create the new backend - backend, err = c.newLogicalBackend(entry.Type, sysView, view, nil) + backend, err = c.newLogicalBackend(entry.Type, sysView, view, conf) if err != nil { c.logger.Error("core: failed to create mount entry", "path", entry.Path, "error", err) return errLoadMountsFailed @@ -669,6 +693,12 @@ func (c *Core) setupMounts() error { return fmt.Errorf("created mount entry of type %q is nil", entry.Type) } + // Check for the correct backend type + backendType := backend.Type() + if entry.Type == "plugin" && backendType != logical.TypeLogical { + return fmt.Errorf("cannot mount '%s' of type '%s' as a logical backend", entry.Config.PluginName, backendType) + } + if err := backend.Initialize(); err != nil { return err } @@ -687,10 +717,9 @@ func (c *Core) setupMounts() error { if err != nil { c.logger.Error("core: failed to mount entry", "path", entry.Path, "error", err) return errLoadMountsFailed - } else { - if c.logger.IsInfo() { - c.logger.Info("core: successfully mounted backend", "type", entry.Type, "path", entry.Path) - } + } + if c.logger.IsInfo() { + c.logger.Info("core: successfully mounted backend", "type", entry.Type, "path", entry.Path) } // Ensure the path is tainted if set in the mount table diff --git a/vault/router_test.go b/vault/router_test.go index 7b6ed30363..acf4fcce03 100644 --- a/vault/router_test.go +++ b/vault/router_test.go @@ -2,6 +2,7 @@ package vault import ( "fmt" + "io/ioutil" "reflect" "strings" "sync" @@ -9,7 +10,9 @@ import ( "time" "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/logformat" "github.com/hashicorp/vault/logical" + log "github.com/mgutz/logxi/v1" ) type NoopBackend struct { @@ -63,10 +66,26 @@ func (n *NoopBackend) InvalidateKey(k string) { n.Invalidations = append(n.Invalidations, k) } +func (n *NoopBackend) Setup(config *logical.BackendConfig) error { + return nil +} + +func (n *NoopBackend) Logger() log.Logger { + return logformat.NewVaultLoggerWithWriter(ioutil.Discard, log.LevelOff) +} + func (n *NoopBackend) Initialize() error { return nil } +func (n *NoopBackend) Type() logical.BackendType { + return logical.TypeLogical +} + +func (n *NoopBackend) RegisterLicense(license interface{}) error { + return nil +} + func TestRouter_Mount(t *testing.T) { r := NewRouter() _, barrier, _ := mockBarrier(t) diff --git a/vault/testing.go b/vault/testing.go index 9ccf7e6632..d3b75c2999 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -520,6 +520,10 @@ func (n *rawHTTP) System() logical.SystemView { } } +func (n *rawHTTP) Logger() log.Logger { + return logformat.NewVaultLogger(log.LevelTrace) +} + func (n *rawHTTP) Cleanup() { // noop } @@ -533,6 +537,19 @@ func (n *rawHTTP) InvalidateKey(string) { // noop } +func (n *rawHTTP) Setup(config *logical.BackendConfig) error { + // noop + return nil +} + +func (n *rawHTTP) Type() logical.BackendType { + return logical.TypeUnknown +} + +func (n *rawHTTP) RegisterLicense(license interface{}) error { + return nil +} + func GenerateRandBytes(length int) ([]byte, error) { if length < 0 { return nil, fmt.Errorf("length must be >= 0") diff --git a/website/source/docs/plugin/index.html.md b/website/source/docs/plugin/index.html.md new file mode 100644 index 0000000000..03365d53ec --- /dev/null +++ b/website/source/docs/plugin/index.html.md @@ -0,0 +1,44 @@ +--- +layout: "docs" +page_title: "Plugin Backends" +sidebar_current: "docs-plugin" +description: |- + Plugin backends are mountable backends that are implemented unsing Vault's plugin system. +--- + +# Plugin Backends + +Plugin backends are the components in Vault that can be implemented separately from Vault's +builtin backends. These backends can be either authentication or secret backends. + +Detailed information regarding the plugin system can be found in the +[internals documentation](https://www.vaultproject.io/docs/internals/plugins.html). + +# Mounting/unmounting Plugin Backends + +Before a plugin backend can be mounted, it needs to be registered via the +[plugin catalog](https://www.vaultproject.io/docs/internals/plugins.html#plugin-catalog). After +the plugin is registered, it can be mounted by specifying the registered plugin name: + +``` +$ vault mount -path=my-secrets -plugin-name=passthrough-plugin plugin +Successfully mounted plugin 'passthrough-plugin' at 'my-secrets'! +``` + +Listing mounts will display backends that are mounted as plugins, along with the +name of plugin backend that is mounted: + +``` +$ vault mounts +Path Type Accessor Plugin Default TTL Max TTL Force No Cache Replication Behavior Description +my-secrets/ plugin plugin_deb84140 passthrough-plugin system system false replicated +... +``` + +Unmounting a plugin backend is the identical to unmounting internal backends: + +``` +$ vault unmount my-secrets +``` + + diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 377e6188f4..0fac3b87f3 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -316,6 +316,10 @@ + > + Plugin Backends + +
>