// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "context" "encoding/base64" "net" "net/http" "strings" "testing" "time" log "github.com/hashicorp/go-hclog" kv "github.com/hashicorp/vault-plugin-secrets-kv" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/audit" "github.com/hashicorp/vault/builtin/logical/pki" "github.com/hashicorp/vault/builtin/logical/ssh" "github.com/hashicorp/vault/builtin/logical/transit" "github.com/hashicorp/vault/helper/benchhelpers" "github.com/hashicorp/vault/helper/builtinplugins" "github.com/hashicorp/vault/sdk/helper/logging" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/physical/inmem" "github.com/hashicorp/vault/vault" "github.com/hashicorp/vault/vault/seal" "github.com/mitchellh/cli" auditFile "github.com/hashicorp/vault/builtin/audit/file" credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" vaulthttp "github.com/hashicorp/vault/http" ) var ( defaultVaultLogger = log.NewNullLogger() defaultVaultCredentialBackends = map[string]logical.Factory{ "userpass": credUserpass.Factory, } defaultVaultAuditBackends = map[string]audit.Factory{ "file": auditFile.Factory, } defaultVaultLogicalBackends = map[string]logical.Factory{ "generic-leased": vault.LeasedPassthroughBackendFactory, "pki": pki.Factory, "ssh": ssh.Factory, "transit": transit.Factory, "kv": kv.Factory, } ) // assertNoTabs asserts the CLI help has no tab characters. func assertNoTabs(tb testing.TB, c cli.Command) { tb.Helper() if strings.ContainsRune(c.Help(), '\t') { tb.Errorf("%#v help output contains tabs", c) } } // testVaultServer creates a test vault cluster and returns a configured API // client and closer function. func testVaultServer(tb testing.TB) (*api.Client, func()) { tb.Helper() client, _, closer := testVaultServerUnseal(tb) return client, closer } func testVaultServerWithSecrets(ctx context.Context, tb testing.TB) (*api.Client, func()) { tb.Helper() client, _, closer := testVaultServerUnseal(tb) // enable kv-v1 backend if err := client.Sys().Mount("kv-v1/", &api.MountInput{ Type: "kv-v1", }); err != nil { tb.Fatal(err) } // enable kv-v2 backend if err := client.Sys().Mount("kv-v2/", &api.MountInput{ Type: "kv-v2", }); err != nil { tb.Fatal(err) } // populate dummy secrets for _, path := range []string{ "foo", "app-1/foo", "app-1/bar", "app-1/nested/baz", } { if err := client.KVv1("kv-v1").Put(ctx, path, map[string]interface{}{ "user": "test", "password": "Hashi123", }); err != nil { tb.Fatal(err) } if _, err := client.KVv2("kv-v2").Put(ctx, path, map[string]interface{}{ "user": "test", "password": "Hashi123", }); err != nil { tb.Fatal(err) } } return client, closer } func testVaultServerWithKVVersion(tb testing.TB, kvVersion string) (*api.Client, func()) { tb.Helper() client, _, closer := testVaultServerUnsealWithKVVersionWithSeal(tb, kvVersion, nil) return client, closer } func testVaultServerAllBackends(tb testing.TB) (*api.Client, func()) { tb.Helper() client, _, closer := testVaultServerCoreConfig(tb, &vault.CoreConfig{ DisableMlock: true, DisableCache: true, Logger: defaultVaultLogger, CredentialBackends: credentialBackends, AuditBackends: auditBackends, LogicalBackends: logicalBackends, BuiltinRegistry: builtinplugins.Registry, }) return client, closer } // testVaultServerAutoUnseal creates a test vault cluster and sets it up with auto unseal // the function returns a client, the recovery keys, and a closer function func testVaultServerAutoUnseal(tb testing.TB) (*api.Client, []string, func()) { testSeal, _ := seal.NewTestSeal(nil) autoSeal, err := vault.NewAutoSeal(testSeal) if err != nil { tb.Fatal("unable to create autoseal", err) } return testVaultServerUnsealWithKVVersionWithSeal(tb, "1", autoSeal) } // testVaultServerUnseal creates a test vault cluster and returns a configured // API client, list of unseal keys (as strings), and a closer function. func testVaultServerUnseal(tb testing.TB) (*api.Client, []string, func()) { return testVaultServerUnsealWithKVVersionWithSeal(tb, "1", nil) } func testVaultServerUnsealWithKVVersionWithSeal(tb testing.TB, kvVersion string, seal vault.Seal) (*api.Client, []string, func()) { tb.Helper() logger := log.NewInterceptLogger(&log.LoggerOptions{ Output: log.DefaultOutput, Level: log.Debug, JSONFormat: logging.ParseEnvLogFormat() == logging.JSONFormat, }) return testVaultServerCoreConfigWithOpts(tb, &vault.CoreConfig{ DisableMlock: true, DisableCache: true, Logger: logger, CredentialBackends: defaultVaultCredentialBackends, AuditBackends: defaultVaultAuditBackends, LogicalBackends: defaultVaultLogicalBackends, BuiltinRegistry: builtinplugins.Registry, Seal: seal, }, &vault.TestClusterOptions{ HandlerFunc: vaulthttp.Handler, NumCores: 1, KVVersion: kvVersion, }) } // testVaultServerUnseal creates a test vault cluster and returns a configured // API client, list of unseal keys (as strings), and a closer function // configured with the given plugin directory. func testVaultServerPluginDir(tb testing.TB, pluginDir string) (*api.Client, []string, func()) { tb.Helper() return testVaultServerCoreConfig(tb, &vault.CoreConfig{ DisableMlock: true, DisableCache: true, Logger: defaultVaultLogger, CredentialBackends: defaultVaultCredentialBackends, AuditBackends: defaultVaultAuditBackends, LogicalBackends: defaultVaultLogicalBackends, PluginDirectory: pluginDir, BuiltinRegistry: builtinplugins.Registry, }) } func testVaultServerCoreConfig(tb testing.TB, coreConfig *vault.CoreConfig) (*api.Client, []string, func()) { return testVaultServerCoreConfigWithOpts(tb, coreConfig, &vault.TestClusterOptions{ HandlerFunc: vaulthttp.Handler, NumCores: 1, // Default is 3, but we don't need that many }) } // testVaultServerCoreConfig creates a new vault cluster with the given core // configuration. This is a lower-level test helper. If the seal config supports recovery keys, then // recovery keys are returned. Otherwise, unseal keys are returned func testVaultServerCoreConfigWithOpts(tb testing.TB, coreConfig *vault.CoreConfig, opts *vault.TestClusterOptions) (*api.Client, []string, func()) { tb.Helper() cluster := vault.NewTestCluster(benchhelpers.TBtoT(tb), coreConfig, opts) cluster.Start() // Make it easy to get access to the active core := cluster.Cores[0].Core vault.TestWaitActive(benchhelpers.TBtoT(tb), core) // Get the client already setup for us! client := cluster.Cores[0].Client client.SetToken(cluster.RootToken) var keys [][]byte if coreConfig.Seal != nil && coreConfig.Seal.RecoveryKeySupported() { keys = cluster.RecoveryKeys } else { keys = cluster.BarrierKeys } return client, encodeKeys(keys), cluster.Cleanup } // Convert the unseal keys to base64 encoded, since these are how the user // will get them. func encodeKeys(rawKeys [][]byte) []string { keys := make([]string, len(rawKeys)) for i := range rawKeys { keys[i] = base64.StdEncoding.EncodeToString(rawKeys[i]) } return keys } // testVaultServerUninit creates an uninitialized server. func testVaultServerUninit(tb testing.TB) (*api.Client, func()) { tb.Helper() inm, err := inmem.NewInmem(nil, defaultVaultLogger) if err != nil { tb.Fatal(err) } core, err := vault.NewCore(&vault.CoreConfig{ DisableMlock: true, DisableCache: true, Logger: defaultVaultLogger, Physical: inm, CredentialBackends: defaultVaultCredentialBackends, AuditBackends: defaultVaultAuditBackends, LogicalBackends: defaultVaultLogicalBackends, BuiltinRegistry: builtinplugins.Registry, }) if err != nil { tb.Fatal(err) } ln, addr := vaulthttp.TestServer(tb, core) client, err := api.NewClient(&api.Config{ Address: addr, }) if err != nil { tb.Fatal(err) } closer := func() { core.Shutdown() ln.Close() } return client, closer } // testVaultServerBad creates an http server that returns a 500 on each request // to simulate failures. func testVaultServerBad(tb testing.TB) (*api.Client, func()) { tb.Helper() listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { tb.Fatal(err) } server := &http.Server{ Addr: "127.0.0.1:0", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "500 internal server error", http.StatusInternalServerError) }), ReadTimeout: 1 * time.Second, ReadHeaderTimeout: 1 * time.Second, WriteTimeout: 1 * time.Second, IdleTimeout: 1 * time.Second, } go func() { if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { tb.Fatal(err) } }() client, err := api.NewClient(&api.Config{ Address: "http://" + listener.Addr().String(), }) if err != nil { tb.Fatal(err) } return client, func() { ctx, done := context.WithTimeout(context.Background(), 5*time.Second) defer done() server.Shutdown(ctx) } } // testTokenAndAccessor creates a new authentication token capable of being renewed with // the default policy attached. It returns the token and it's accessor. func testTokenAndAccessor(tb testing.TB, client *api.Client) (string, string) { tb.Helper() secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ Policies: []string{"default"}, TTL: "30m", }) if err != nil { tb.Fatal(err) } if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { tb.Fatalf("missing auth data: %#v", secret) } return secret.Auth.ClientToken, secret.Auth.Accessor } func testClient(tb testing.TB, addr string, token string) *api.Client { tb.Helper() config := api.DefaultConfig() config.Address = addr client, err := api.NewClient(config) if err != nil { tb.Fatal(err) } client.SetToken(token) return client }