diff --git a/builtin/logical/consul/backend.go b/builtin/logical/consul/backend.go deleted file mode 100644 index 52aeb3cbc..000000000 --- a/builtin/logical/consul/backend.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package consul - -import ( - "context" - - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/logical" -) - -const operationPrefixConsul = "consul" - -// ReportedVersion is used to report a specific version to Vault. -var ReportedVersion = "" - -func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { - b := Backend() - if err := b.Setup(ctx, conf); err != nil { - return nil, err - } - return b, nil -} - -func Backend() *backend { - var b backend - b.Backend = &framework.Backend{ - PathsSpecial: &logical.Paths{ - SealWrapStorage: []string{ - "config/access", - }, - }, - - Paths: []*framework.Path{ - pathConfigAccess(&b), - pathListRoles(&b), - pathRoles(&b), - pathToken(&b), - }, - - Secrets: []*framework.Secret{ - secretToken(&b), - }, - BackendType: logical.TypeLogical, - RunningVersion: ReportedVersion, - } - - return &b -} - -type backend struct { - *framework.Backend -} diff --git a/builtin/logical/consul/backend_test.go b/builtin/logical/consul/backend_test.go deleted file mode 100644 index aa377f26e..000000000 --- a/builtin/logical/consul/backend_test.go +++ /dev/null @@ -1,1613 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package consul - -import ( - "context" - "encoding/base64" - "fmt" - "log" - "os" - "reflect" - "strings" - "testing" - "time" - - consulapi "github.com/hashicorp/consul/api" - "github.com/hashicorp/vault/helper/testhelpers/consul" - logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" - "github.com/hashicorp/vault/sdk/logical" - "github.com/mitchellh/mapstructure" -) - -func TestBackend_Config_Access(t *testing.T) { - t.Run("config_access", func(t *testing.T) { - t.Parallel() - t.Run("pre-1.4.0", func(t *testing.T) { - t.Parallel() - testBackendConfigAccess(t, "1.3.1", true) - }) - t.Run("post-1.4.0", func(t *testing.T) { - t.Parallel() - testBackendConfigAccess(t, "", true) - }) - t.Run("pre-1.4.0 automatic-bootstrap", func(t *testing.T) { - t.Parallel() - testBackendConfigAccess(t, "1.3.1", false) - }) - t.Run("post-1.4.0 automatic-bootstrap", func(t *testing.T) { - t.Parallel() - testBackendConfigAccess(t, "", false) - }) - }) -} - -func testBackendConfigAccess(t *testing.T, version string, autoBootstrap bool) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, consulConfig := consul.PrepareTestContainer(t, version, false, autoBootstrap) - defer cleanup() - - connData := map[string]interface{}{ - "address": consulConfig.Address(), - } - if autoBootstrap || strings.HasPrefix(version, "1.3") { - connData["token"] = consulConfig.Token - } - - confReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/access", - Storage: config.StorageView, - Data: connData, - } - - resp, err := b.HandleRequest(context.Background(), confReq) - if err != nil || (resp != nil && resp.IsError()) || resp != nil { - t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) - } - - confReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), confReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) - } - - expected := map[string]interface{}{ - "address": connData["address"].(string), - "scheme": "http", - } - if !reflect.DeepEqual(expected, resp.Data) { - t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) - } - if resp.Data["token"] != nil { - t.Fatalf("token should not be set in the response") - } -} - -func TestBackend_Renew_Revoke(t *testing.T) { - t.Run("renew_revoke", func(t *testing.T) { - t.Parallel() - t.Run("pre-1.4.0", func(t *testing.T) { - t.Parallel() - testBackendRenewRevoke(t, "1.3.1") - }) - t.Run("post-1.4.0", func(t *testing.T) { - t.Parallel() - t.Run("legacy", func(t *testing.T) { - t.Parallel() - testBackendRenewRevoke(t, "1.4.4") - }) - - t.Run("param-policies", func(t *testing.T) { - t.Parallel() - testBackendRenewRevoke14(t, "", "policies") - }) - t.Run("param-consul_policies", func(t *testing.T) { - t.Parallel() - testBackendRenewRevoke14(t, "", "consul_policies") - }) - t.Run("both-params", func(t *testing.T) { - t.Parallel() - testBackendRenewRevoke14(t, "", "both") - }) - }) - }) -} - -func testBackendRenewRevoke(t *testing.T, version string) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, consulConfig := consul.PrepareTestContainer(t, version, false, true) - defer cleanup() - - connData := map[string]interface{}{ - "address": consulConfig.Address(), - "token": consulConfig.Token, - } - - req := &logical.Request{ - Storage: config.StorageView, - Operation: logical.UpdateOperation, - Path: "config/access", - Data: connData, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req.Path = "roles/test" - req.Data = map[string]interface{}{ - "policy": base64.StdEncoding.EncodeToString([]byte(testPolicy)), - "lease": "6h", - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.ReadOperation - req.Path = "creds/test" - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("resp nil") - } - if resp.IsError() { - t.Fatalf("resp is error: %v", resp.Error()) - } - - generatedSecret := resp.Secret - generatedSecret.TTL = 6 * time.Hour - - var d struct { - Token string `mapstructure:"token"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - t.Fatal(err) - } - - // Build a client and verify that the credentials work - consulapiConfig := consulapi.DefaultConfig() - consulapiConfig.Address = connData["address"].(string) - consulapiConfig.Token = d.Token - client, err := consulapi.NewClient(consulapiConfig) - if err != nil { - t.Fatal(err) - } - - _, err = client.KV().Put(&consulapi.KVPair{ - Key: "foo", - Value: []byte("bar"), - }, nil) - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.RenewOperation - req.Secret = generatedSecret - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response from renew") - } - - req.Operation = logical.RevokeOperation - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - _, err = client.KV().Put(&consulapi.KVPair{ - Key: "foo", - Value: []byte("bar"), - }, nil) - if err == nil { - t.Fatal("err: expected error") - } -} - -func testBackendRenewRevoke14(t *testing.T, version string, policiesParam string) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, consulConfig := consul.PrepareTestContainer(t, version, false, true) - defer cleanup() - - connData := map[string]interface{}{ - "address": consulConfig.Address(), - "token": consulConfig.Token, - } - - req := &logical.Request{ - Storage: config.StorageView, - Operation: logical.UpdateOperation, - Path: "config/access", - Data: connData, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req.Path = "roles/test" - req.Data = map[string]interface{}{ - "lease": "6h", - } - if policiesParam == "both" { - req.Data["policies"] = []string{"wrong-name"} - req.Data["consul_policies"] = []string{"test"} - } else { - req.Data[policiesParam] = []string{"test"} - } - - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - read := &logical.Request{ - Storage: config.StorageView, - Operation: logical.ReadOperation, - Path: "roles/test", - Data: connData, - } - roleResp, err := b.HandleRequest(context.Background(), read) - - expectExtract := roleResp.Data["consul_policies"] - respExtract := roleResp.Data[policiesParam] - if respExtract != nil { - if expectExtract.([]string)[0] != respExtract.([]string)[0] { - t.Errorf("mismatch: response consul_policies '%s' does not match '[test]'", roleResp.Data["consul_policies"]) - } - } - - req.Operation = logical.ReadOperation - req.Path = "creds/test" - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("resp nil") - } - if resp.IsError() { - t.Fatalf("resp is error: %v", resp.Error()) - } - - generatedSecret := resp.Secret - generatedSecret.TTL = 6 * time.Hour - - var d struct { - Token string `mapstructure:"token"` - Accessor string `mapstructure:"accessor"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - t.Fatal(err) - } - - // Build a client and verify that the credentials work - consulapiConfig := consulapi.DefaultNonPooledConfig() - consulapiConfig.Address = connData["address"].(string) - consulapiConfig.Token = d.Token - client, err := consulapi.NewClient(consulapiConfig) - if err != nil { - t.Fatal(err) - } - - _, err = client.Catalog(), nil - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.RenewOperation - req.Secret = generatedSecret - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response from renew") - } - - req.Operation = logical.RevokeOperation - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Build a management client and verify that the token does not exist anymore - consulmgmtConfig := consulapi.DefaultNonPooledConfig() - consulmgmtConfig.Address = connData["address"].(string) - consulmgmtConfig.Token = connData["token"].(string) - mgmtclient, err := consulapi.NewClient(consulmgmtConfig) - if err != nil { - t.Fatal(err) - } - q := &consulapi.QueryOptions{ - Datacenter: "DC1", - } - - _, _, err = mgmtclient.ACL().TokenRead(d.Accessor, q) - if err == nil { - t.Fatal("err: expected error") - } -} - -func TestBackend_LocalToken(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, consulConfig := consul.PrepareTestContainer(t, "", false, true) - defer cleanup() - - connData := map[string]interface{}{ - "address": consulConfig.Address(), - "token": consulConfig.Token, - } - - req := &logical.Request{ - Storage: config.StorageView, - Operation: logical.UpdateOperation, - Path: "config/access", - Data: connData, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req.Path = "roles/test" - req.Data = map[string]interface{}{ - "consul_policies": []string{"test"}, - "ttl": "6h", - "local": false, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req.Path = "roles/test_local" - req.Data = map[string]interface{}{ - "consul_policies": []string{"test"}, - "ttl": "6h", - "local": true, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.ReadOperation - req.Path = "creds/test" - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("resp nil") - } - if resp.IsError() { - t.Fatalf("resp is error: %v", resp.Error()) - } - - var d struct { - Token string `mapstructure:"token"` - Accessor string `mapstructure:"accessor"` - Local bool `mapstructure:"local"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - t.Fatal(err) - } - - if d.Local { - t.Fatalf("requested global token, got local one") - } - - // Build a client and verify that the credentials work - consulapiConfig := consulapi.DefaultNonPooledConfig() - consulapiConfig.Address = connData["address"].(string) - consulapiConfig.Token = d.Token - client, err := consulapi.NewClient(consulapiConfig) - if err != nil { - t.Fatal(err) - } - - _, err = client.Catalog(), nil - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.ReadOperation - req.Path = "creds/test_local" - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("resp nil") - } - if resp.IsError() { - t.Fatalf("resp is error: %v", resp.Error()) - } - - if err := mapstructure.Decode(resp.Data, &d); err != nil { - t.Fatal(err) - } - - if !d.Local { - t.Fatalf("requested local token, got global one") - } - - // Build a client and verify that the credentials work - consulapiConfig = consulapi.DefaultNonPooledConfig() - consulapiConfig.Address = connData["address"].(string) - consulapiConfig.Token = d.Token - client, err = consulapi.NewClient(consulapiConfig) - if err != nil { - t.Fatal(err) - } - - _, err = client.Catalog(), nil - if err != nil { - t.Fatal(err) - } -} - -func TestBackend_Management(t *testing.T) { - t.Run("management", func(t *testing.T) { - t.Parallel() - t.Run("pre-1.4.0", func(t *testing.T) { - t.Parallel() - testBackendManagement(t, "1.3.1") - }) - t.Run("post-1.4.0", func(t *testing.T) { - t.Parallel() - testBackendManagement(t, "1.4.4") - }) - - testBackendManagement(t, "1.10.8") - }) -} - -func testBackendManagement(t *testing.T, version string) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, consulConfig := consul.PrepareTestContainer(t, version, false, true) - defer cleanup() - - connData := map[string]interface{}{ - "address": consulConfig.Address(), - "token": consulConfig.Token, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfig(t, connData), - testAccStepWriteManagementPolicy(t, "test", ""), - testAccStepReadManagementToken(t, "test", connData), - }, - }) -} - -func TestBackend_Basic(t *testing.T) { - t.Run("basic", func(t *testing.T) { - t.Parallel() - t.Run("pre-1.4.0", func(t *testing.T) { - t.Parallel() - testBackendBasic(t, "1.3.1") - }) - t.Run("post-1.4.0", func(t *testing.T) { - t.Parallel() - t.Run("legacy", func(t *testing.T) { - t.Parallel() - testBackendBasic(t, "1.4.4") - }) - - testBackendBasic(t, "1.10.8") - }) - }) -} - -func testBackendBasic(t *testing.T, version string) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, consulConfig := consul.PrepareTestContainer(t, version, false, true) - defer cleanup() - - connData := map[string]interface{}{ - "address": consulConfig.Address(), - "token": consulConfig.Token, - } - - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfig(t, connData), - testAccStepWritePolicy(t, "test", testPolicy, ""), - testAccStepReadToken(t, "test", connData), - }, - }) -} - -func TestBackend_crud(t *testing.T) { - b, _ := Factory(context.Background(), logical.TestBackendConfig()) - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepWritePolicy(t, "test", testPolicy, ""), - testAccStepWritePolicy(t, "test2", testPolicy, ""), - testAccStepWritePolicy(t, "test3", testPolicy, ""), - testAccStepReadPolicy(t, "test", testPolicy, 0), - testAccStepListPolicy(t, []string{"test", "test2", "test3"}), - testAccStepDeletePolicy(t, "test"), - }, - }) -} - -func TestBackend_role_lease(t *testing.T) { - b, _ := Factory(context.Background(), logical.TestBackendConfig()) - logicaltest.Test(t, logicaltest.TestCase{ - LogicalBackend: b, - Steps: []logicaltest.TestStep{ - testAccStepWritePolicy(t, "test", testPolicy, "6h"), - testAccStepReadPolicy(t, "test", testPolicy, 6*time.Hour), - testAccStepDeletePolicy(t, "test"), - }, - }) -} - -func testAccStepConfig(t *testing.T, config map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "config/access", - Data: config, - } -} - -func testAccStepReadToken(t *testing.T, name string, conf map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "creds/" + name, - Check: func(resp *logical.Response) error { - var d struct { - Token string `mapstructure:"token"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - log.Printf("[WARN] Generated token: %s", d.Token) - - // Build a client and verify that the credentials work - config := consulapi.DefaultConfig() - config.Address = conf["address"].(string) - config.Token = d.Token - client, err := consulapi.NewClient(config) - if err != nil { - return err - } - - log.Printf("[WARN] Verifying that the generated token works...") - _, err = client.KV().Put(&consulapi.KVPair{ - Key: "foo", - Value: []byte("bar"), - }, nil) - if err != nil { - return err - } - - return nil - }, - } -} - -func testAccStepReadManagementToken(t *testing.T, name string, conf map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "creds/" + name, - Check: func(resp *logical.Response) error { - var d struct { - Token string `mapstructure:"token"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - log.Printf("[WARN] Generated token: %s", d.Token) - - // Build a client and verify that the credentials work - config := consulapi.DefaultConfig() - config.Address = conf["address"].(string) - config.Token = d.Token - client, err := consulapi.NewClient(config) - if err != nil { - return err - } - - log.Printf("[WARN] Verifying that the generated token works...") - _, _, err = client.ACL().Create(&consulapi.ACLEntry{ - Type: "management", - Name: "test2", - }, nil) - if err != nil { - return err - } - - return nil - }, - } -} - -func testAccStepWritePolicy(t *testing.T, name string, policy string, lease string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "roles/" + name, - Data: map[string]interface{}{ - "policy": base64.StdEncoding.EncodeToString([]byte(policy)), - "lease": lease, - }, - } -} - -func testAccStepWriteManagementPolicy(t *testing.T, name string, lease string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "roles/" + name, - Data: map[string]interface{}{ - "token_type": "management", - "lease": lease, - }, - } -} - -func testAccStepReadPolicy(t *testing.T, name string, policy string, lease time.Duration) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "roles/" + name, - Check: func(resp *logical.Response) error { - policyRaw := resp.Data["policy"].(string) - out, err := base64.StdEncoding.DecodeString(policyRaw) - if err != nil { - return err - } - if string(out) != policy { - return fmt.Errorf("mismatch: %s %s", out, policy) - } - - l := resp.Data["lease"].(int64) - if lease != time.Second*time.Duration(l) { - return fmt.Errorf("mismatch: %v %v", l, lease) - } - return nil - }, - } -} - -func testAccStepListPolicy(t *testing.T, names []string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ListOperation, - Path: "roles/", - Check: func(resp *logical.Response) error { - respKeys := resp.Data["keys"].([]string) - if !reflect.DeepEqual(respKeys, names) { - return fmt.Errorf("mismatch: %#v %#v", respKeys, names) - } - return nil - }, - } -} - -func testAccStepDeletePolicy(t *testing.T, name string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.DeleteOperation, - Path: "roles/" + name, - } -} - -func TestBackend_Roles(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, consulConfig := consul.PrepareTestContainer(t, "", false, true) - defer cleanup() - - connData := map[string]interface{}{ - "address": consulConfig.Address(), - "token": consulConfig.Token, - } - - req := &logical.Request{ - Storage: config.StorageView, - Operation: logical.UpdateOperation, - Path: "config/access", - Data: connData, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Create the consul_roles role - req.Path = "roles/test-consul-roles" - req.Data = map[string]interface{}{ - "consul_roles": []string{"role-test"}, - "lease": "6h", - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.ReadOperation - req.Path = "creds/test-consul-roles" - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("resp nil") - } - if resp.IsError() { - t.Fatalf("resp is error: %v", resp.Error()) - } - - generatedSecret := resp.Secret - generatedSecret.TTL = 6 * time.Hour - - var d struct { - Token string `mapstructure:"token"` - Accessor string `mapstructure:"accessor"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - t.Fatal(err) - } - - // Build a client and verify that the credentials work - consulapiConfig := consulapi.DefaultNonPooledConfig() - consulapiConfig.Address = connData["address"].(string) - consulapiConfig.Token = d.Token - client, err := consulapi.NewClient(consulapiConfig) - if err != nil { - t.Fatal(err) - } - - _, err = client.Catalog(), nil - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.RenewOperation - req.Secret = generatedSecret - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response from renew") - } - - req.Operation = logical.RevokeOperation - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Build a management client and verify that the token does not exist anymore - consulmgmtConfig := consulapi.DefaultNonPooledConfig() - consulmgmtConfig.Address = connData["address"].(string) - consulmgmtConfig.Token = connData["token"].(string) - mgmtclient, err := consulapi.NewClient(consulmgmtConfig) - if err != nil { - t.Fatal(err) - } - q := &consulapi.QueryOptions{ - Datacenter: "DC1", - } - - _, _, err = mgmtclient.ACL().TokenRead(d.Accessor, q) - if err == nil { - t.Fatal("err: expected error") - } -} - -func TestBackend_Enterprise_Diff_Namespace_Revocation(t *testing.T) { - if _, hasLicense := os.LookupEnv("CONSUL_LICENSE"); !hasLicense { - t.Skip("Skipping: No enterprise license found") - } - - testBackendEntDiffNamespaceRevocation(t) -} - -func TestBackend_Enterprise_Diff_Partition_Revocation(t *testing.T) { - if _, hasLicense := os.LookupEnv("CONSUL_LICENSE"); !hasLicense { - t.Skip("Skipping: No enterprise license found") - } - - testBackendEntDiffPartitionRevocation(t) -} - -func TestBackend_Enterprise_Namespace(t *testing.T) { - if _, hasLicense := os.LookupEnv("CONSUL_LICENSE"); !hasLicense { - t.Skip("Skipping: No enterprise license found") - } - - testBackendEntNamespace(t) -} - -func TestBackend_Enterprise_Partition(t *testing.T) { - if _, hasLicense := os.LookupEnv("CONSUL_LICENSE"); !hasLicense { - t.Skip("Skipping: No enterprise license found") - } - - testBackendEntPartition(t) -} - -func testBackendEntDiffNamespaceRevocation(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, consulConfig := consul.PrepareTestContainer(t, "", true, true) - defer cleanup() - - // Perform additional Consul configuration - consulapiConfig := consulapi.DefaultNonPooledConfig() - consulapiConfig.Address = consulConfig.Address() - consulapiConfig.Token = consulConfig.Token - client, err := consulapi.NewClient(consulapiConfig) - if err != nil { - t.Fatal(err) - } - - // Create Policy in default namespace to manage ACLs in a different - // namespace - nsPol := &consulapi.ACLPolicy{ - Name: "diff-ns-test", - Description: "policy to test management of ACLs in one ns from another", - Rules: `namespace "ns1" { - acl="write" - } - `, - } - pol, _, err := client.ACL().PolicyCreate(nsPol, nil) - if err != nil { - t.Fatal(err) - } - - // Create new Token in default namespace with new ACL - cToken, _, err := client.ACL().TokenCreate( - &consulapi.ACLToken{ - Policies: []*consulapi.ACLLink{{ID: pol.ID}}, - }, nil) - if err != nil { - t.Fatal(err) - } - - // Write backend config - connData := map[string]interface{}{ - "address": consulConfig.Address(), - "token": cToken.SecretID, - } - - req := &logical.Request{ - Storage: config.StorageView, - Operation: logical.UpdateOperation, - Path: "config/access", - Data: connData, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Create the role in namespace "ns1" - req.Path = "roles/test-ns" - req.Data = map[string]interface{}{ - "consul_policies": []string{"ns-test"}, - "lease": "6h", - "consul_namespace": "ns1", - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Get Token - req.Operation = logical.ReadOperation - req.Path = "creds/test-ns" - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("resp nil") - } - if resp.IsError() { - t.Fatalf("resp is error: %v", resp.Error()) - } - - generatedSecret := resp.Secret - generatedSecret.TTL = 6 * time.Hour - - // Verify Secret - var d struct { - Token string `mapstructure:"token"` - Accessor string `mapstructure:"accessor"` - ConsulNamespace string `mapstructure:"consul_namespace"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - t.Fatal(err) - } - - if d.ConsulNamespace != "ns1" { - t.Fatalf("Failed to access namespace") - } - - // Revoke the credential - req.Operation = logical.RevokeOperation - req.Secret = generatedSecret - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("Revocation failed: %v", err) - } - - // Build a management client and verify that the token does not exist anymore - consulmgmtConfig := consulapi.DefaultNonPooledConfig() - consulmgmtConfig.Address = connData["address"].(string) - consulmgmtConfig.Token = connData["token"].(string) - mgmtclient, err := consulapi.NewClient(consulmgmtConfig) - if err != nil { - t.Fatal(err) - } - q := &consulapi.QueryOptions{ - Datacenter: "DC1", - Namespace: "ns1", - } - - _, _, err = mgmtclient.ACL().TokenRead(d.Accessor, q) - if err == nil { - t.Fatal("err: expected error") - } -} - -func testBackendEntDiffPartitionRevocation(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, consulConfig := consul.PrepareTestContainer(t, "", true, true) - defer cleanup() - - // Perform additional Consul configuration - consulapiConfig := consulapi.DefaultNonPooledConfig() - consulapiConfig.Address = consulConfig.Address() - consulapiConfig.Token = consulConfig.Token - client, err := consulapi.NewClient(consulapiConfig) - if err != nil { - t.Fatal(err) - } - - // Create Policy in default partition to manage ACLs in a different - // partition - partPol := &consulapi.ACLPolicy{ - Name: "diff-part-test", - Description: "policy to test management of ACLs in one part from another", - Rules: `partition "part1" { - acl="write" - } - `, - } - pol, _, err := client.ACL().PolicyCreate(partPol, nil) - if err != nil { - t.Fatal(err) - } - - // Create new Token in default partition with new ACL - cToken, _, err := client.ACL().TokenCreate( - &consulapi.ACLToken{ - Policies: []*consulapi.ACLLink{{ID: pol.ID}}, - }, nil) - if err != nil { - t.Fatal(err) - } - - // Write backend config - connData := map[string]interface{}{ - "address": consulConfig.Address(), - "token": cToken.SecretID, - } - - req := &logical.Request{ - Storage: config.StorageView, - Operation: logical.UpdateOperation, - Path: "config/access", - Data: connData, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Create the role in partition "part1" - req.Path = "roles/test-part" - req.Data = map[string]interface{}{ - "consul_policies": []string{"part-test"}, - "lease": "6h", - "partition": "part1", - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Get Token - req.Operation = logical.ReadOperation - req.Path = "creds/test-part" - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("resp nil") - } - if resp.IsError() { - t.Fatalf("resp is error: %v", resp.Error()) - } - - generatedSecret := resp.Secret - generatedSecret.TTL = 6 * time.Hour - - // Verify Secret - var d struct { - Token string `mapstructure:"token"` - Accessor string `mapstructure:"accessor"` - Partition string `mapstructure:"partition"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - t.Fatal(err) - } - - if d.Partition != "part1" { - t.Fatalf("Failed to access partition") - } - - // Revoke the credential - req.Operation = logical.RevokeOperation - req.Secret = generatedSecret - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("Revocation failed: %v", err) - } - - // Build a management client and verify that the token does not exist anymore - consulmgmtConfig := consulapi.DefaultNonPooledConfig() - consulmgmtConfig.Address = connData["address"].(string) - consulmgmtConfig.Token = connData["token"].(string) - mgmtclient, err := consulapi.NewClient(consulmgmtConfig) - if err != nil { - t.Fatal(err) - } - q := &consulapi.QueryOptions{ - Datacenter: "DC1", - Partition: "part1", - } - - _, _, err = mgmtclient.ACL().TokenRead(d.Accessor, q) - if err == nil { - t.Fatal("err: expected error") - } -} - -func testBackendEntNamespace(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, consulConfig := consul.PrepareTestContainer(t, "", true, true) - defer cleanup() - - connData := map[string]interface{}{ - "address": consulConfig.Address(), - "token": consulConfig.Token, - } - - req := &logical.Request{ - Storage: config.StorageView, - Operation: logical.UpdateOperation, - Path: "config/access", - Data: connData, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Create the role in namespace "ns1" - req.Path = "roles/test-ns" - req.Data = map[string]interface{}{ - "consul_policies": []string{"ns-test"}, - "lease": "6h", - "consul_namespace": "ns1", - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.ReadOperation - req.Path = "creds/test-ns" - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("resp nil") - } - if resp.IsError() { - t.Fatalf("resp is error: %v", resp.Error()) - } - - generatedSecret := resp.Secret - generatedSecret.TTL = 6 * time.Hour - - var d struct { - Token string `mapstructure:"token"` - Accessor string `mapstructure:"accessor"` - ConsulNamespace string `mapstructure:"consul_namespace"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - t.Fatal(err) - } - - if d.ConsulNamespace != "ns1" { - t.Fatalf("Failed to access namespace") - } - - // Build a client and verify that the credentials work - consulapiConfig := consulapi.DefaultNonPooledConfig() - consulapiConfig.Address = connData["address"].(string) - consulapiConfig.Token = d.Token - client, err := consulapi.NewClient(consulapiConfig) - if err != nil { - t.Fatal(err) - } - - _, err = client.Catalog(), nil - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.RenewOperation - req.Secret = generatedSecret - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response from renew") - } - - req.Operation = logical.RevokeOperation - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Build a management client and verify that the token does not exist anymore - consulmgmtConfig := consulapi.DefaultNonPooledConfig() - consulmgmtConfig.Address = connData["address"].(string) - consulmgmtConfig.Token = connData["token"].(string) - mgmtclient, err := consulapi.NewClient(consulmgmtConfig) - if err != nil { - t.Fatal(err) - } - q := &consulapi.QueryOptions{ - Datacenter: "DC1", - Namespace: "ns1", - } - - _, _, err = mgmtclient.ACL().TokenRead(d.Accessor, q) - if err == nil { - t.Fatal("err: expected error") - } -} - -func testBackendEntPartition(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, consulConfig := consul.PrepareTestContainer(t, "", true, true) - defer cleanup() - - connData := map[string]interface{}{ - "address": consulConfig.Address(), - "token": consulConfig.Token, - } - - req := &logical.Request{ - Storage: config.StorageView, - Operation: logical.UpdateOperation, - Path: "config/access", - Data: connData, - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Create the role in partition "part1" - req.Path = "roles/test-part" - req.Data = map[string]interface{}{ - "consul_policies": []string{"part-test"}, - "lease": "6h", - "partition": "part1", - } - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.ReadOperation - req.Path = "creds/test-part" - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("resp nil") - } - if resp.IsError() { - t.Fatalf("resp is error: %v", resp.Error()) - } - - generatedSecret := resp.Secret - generatedSecret.TTL = 6 * time.Hour - - var d struct { - Token string `mapstructure:"token"` - Accessor string `mapstructure:"accessor"` - Partition string `mapstructure:"partition"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - t.Fatal(err) - } - - if d.Partition != "part1" { - t.Fatalf("Failed to access partition") - } - - // Build a client and verify that the credentials work - consulapiConfig := consulapi.DefaultNonPooledConfig() - consulapiConfig.Address = connData["address"].(string) - consulapiConfig.Token = d.Token - client, err := consulapi.NewClient(consulapiConfig) - if err != nil { - t.Fatal(err) - } - - _, err = client.Catalog(), nil - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.RenewOperation - req.Secret = generatedSecret - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response from renew") - } - - req.Operation = logical.RevokeOperation - _, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Build a management client and verify that the token does not exist anymore - consulmgmtConfig := consulapi.DefaultNonPooledConfig() - consulmgmtConfig.Address = connData["address"].(string) - consulmgmtConfig.Token = connData["token"].(string) - mgmtclient, err := consulapi.NewClient(consulmgmtConfig) - if err != nil { - t.Fatal(err) - } - q := &consulapi.QueryOptions{ - Datacenter: "DC1", - Partition: "test1", - } - - _, _, err = mgmtclient.ACL().TokenRead(d.Accessor, q) - if err == nil { - t.Fatal("err: expected error") - } -} - -func TestBackendRenewRevokeRolesAndIdentities(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cleanup, consulConfig := consul.PrepareTestContainer(t, "", false, true) - defer cleanup() - - connData := map[string]interface{}{ - "address": consulConfig.Address(), - "token": consulConfig.Token, - } - - req := &logical.Request{ - Storage: config.StorageView, - Operation: logical.UpdateOperation, - Path: "config/access", - Data: connData, - } - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - cases := map[string]struct { - RoleName string - RoleData map[string]interface{} - }{ - "just role": { - "r", - map[string]interface{}{ - "consul_roles": []string{"role-test"}, - "lease": "6h", - }, - }, - "role and policies": { - "rp", - map[string]interface{}{ - "consul_policies": []string{"test"}, - "consul_roles": []string{"role-test"}, - "lease": "6h", - }, - }, - "service identity": { - "si", - map[string]interface{}{ - "service_identities": "service1", - "lease": "6h", - }, - }, - "service identity and policies": { - "sip", - map[string]interface{}{ - "consul_policies": []string{"test"}, - "service_identities": "service1", - "lease": "6h", - }, - }, - "service identity and role": { - "sir", - map[string]interface{}{ - "consul_roles": []string{"role-test"}, - "service_identities": "service1", - "lease": "6h", - }, - }, - "service identity and role and policies": { - "sirp", - map[string]interface{}{ - "consul_policies": []string{"test"}, - "consul_roles": []string{"role-test"}, - "service_identities": "service1", - "lease": "6h", - }, - }, - "node identity": { - "ni", - map[string]interface{}{ - "node_identities": []string{"node1:dc1"}, - "lease": "6h", - }, - }, - "node identity and policies": { - "nip", - map[string]interface{}{ - "consul_policies": []string{"test"}, - "node_identities": []string{"node1:dc1"}, - "lease": "6h", - }, - }, - "node identity and role": { - "nir", - map[string]interface{}{ - "consul_roles": []string{"role-test"}, - "node_identities": []string{"node1:dc1"}, - "lease": "6h", - }, - }, - "node identity and role and policies": { - "nirp", - map[string]interface{}{ - "consul_policies": []string{"test"}, - "consul_roles": []string{"role-test"}, - "node_identities": []string{"node1:dc1"}, - "lease": "6h", - }, - }, - "node identity and service identity": { - "nisi", - map[string]interface{}{ - "service_identities": "service1", - "node_identities": []string{"node1:dc1"}, - "lease": "6h", - }, - }, - "node identity and service identity and policies": { - "nisip", - map[string]interface{}{ - "consul_policies": []string{"test"}, - "service_identities": "service1", - "node_identities": []string{"node1:dc1"}, - "lease": "6h", - }, - }, - "node identity and service identity and role": { - "nisir", - map[string]interface{}{ - "consul_roles": []string{"role-test"}, - "service_identities": "service1", - "node_identities": []string{"node1:dc1"}, - "lease": "6h", - }, - }, - "node identity and service identity and role and policies": { - "nisirp", - map[string]interface{}{ - "consul_policies": []string{"test"}, - "consul_roles": []string{"role-test"}, - "service_identities": "service1", - "node_identities": []string{"node1:dc1"}, - "lease": "6h", - }, - }, - } - - for description, tc := range cases { - t.Logf("Testing: %s", description) - - req.Operation = logical.UpdateOperation - req.Path = fmt.Sprintf("roles/%s", tc.RoleName) - req.Data = tc.RoleData - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.ReadOperation - req.Path = fmt.Sprintf("creds/%s", tc.RoleName) - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("resp nil") - } - if resp.IsError() { - t.Fatalf("resp is error: %v", resp.Error()) - } - - generatedSecret := resp.Secret - generatedSecret.TTL = 6 * time.Hour - - var d struct { - Token string `mapstructure:"token"` - Accessor string `mapstructure:"accessor"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - t.Fatal(err) - } - - // Build a client and verify that the credentials work - consulapiConfig := consulapi.DefaultNonPooledConfig() - consulapiConfig.Address = connData["address"].(string) - consulapiConfig.Token = d.Token - client, err := consulapi.NewClient(consulapiConfig) - if err != nil { - t.Fatal(err) - } - - _, err = client.Catalog(), nil - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.RenewOperation - req.Secret = generatedSecret - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response from renew") - } - - req.Operation = logical.RevokeOperation - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - // Build a management client and verify that the token does not exist anymore - consulmgmtConfig := consulapi.DefaultNonPooledConfig() - consulmgmtConfig.Address = connData["address"].(string) - consulmgmtConfig.Token = connData["token"].(string) - mgmtclient, err := consulapi.NewClient(consulmgmtConfig) - - q := &consulapi.QueryOptions{ - Datacenter: "DC1", - } - - _, _, err = mgmtclient.ACL().TokenRead(d.Accessor, q) - if err == nil { - t.Fatal("err: expected error") - } - } -} - -const testPolicy = ` -key "" { - policy = "write" -}` diff --git a/builtin/logical/consul/client.go b/builtin/logical/consul/client.go deleted file mode 100644 index 8a98200af..000000000 --- a/builtin/logical/consul/client.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package consul - -import ( - "context" - "fmt" - - "github.com/hashicorp/consul/api" - "github.com/hashicorp/vault/sdk/logical" -) - -func (b *backend) client(ctx context.Context, s logical.Storage) (*api.Client, error, error) { - conf, userErr, intErr := b.readConfigAccess(ctx, s) - if intErr != nil { - return nil, nil, intErr - } - if userErr != nil { - return nil, userErr, nil - } - if conf == nil { - return nil, nil, fmt.Errorf("no error received but no configuration found") - } - - consulConf := conf.NewConfig() - client, err := api.NewClient(consulConf) - return client, nil, err -} diff --git a/builtin/logical/consul/cmd/consul/main.go b/builtin/logical/consul/cmd/consul/main.go deleted file mode 100644 index 6f0dfe45c..000000000 --- a/builtin/logical/consul/cmd/consul/main.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package main - -import ( - "os" - - hclog "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/builtin/logical/consul" - "github.com/hashicorp/vault/sdk/plugin" -) - -func main() { - apiClientMeta := &api.PluginAPIClientMeta{} - flags := apiClientMeta.FlagSet() - flags.Parse(os.Args[1:]) - - tlsConfig := apiClientMeta.GetTLSConfig() - tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) - - if err := plugin.ServeMultiplex(&plugin.ServeOpts{ - BackendFactoryFunc: consul.Factory, - // set the TLSProviderFunc so that the plugin maintains backwards - // compatibility with Vault versions that don’t support plugin AutoMTLS - TLSProviderFunc: tlsProviderFunc, - }); err != nil { - logger := hclog.New(&hclog.LoggerOptions{}) - - logger.Error("plugin shutting down", "error", err) - os.Exit(1) - } -} diff --git a/builtin/logical/consul/path_config.go b/builtin/logical/consul/path_config.go deleted file mode 100644 index 11da1f222..000000000 --- a/builtin/logical/consul/path_config.go +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package consul - -import ( - "context" - "fmt" - - "github.com/hashicorp/consul/api" - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/logical" -) - -func pathConfigAccess(b *backend) *framework.Path { - return &framework.Path{ - Pattern: "config/access", - - DisplayAttrs: &framework.DisplayAttributes{ - OperationPrefix: operationPrefixConsul, - }, - - Fields: map[string]*framework.FieldSchema{ - "address": { - Type: framework.TypeString, - Description: "Consul server address", - }, - - "scheme": { - Type: framework.TypeString, - Description: "URI scheme for the Consul address", - - // https would be a better default but Consul on its own - // defaults to HTTP access, and when HTTPS is enabled it - // disables HTTP, so there isn't really any harm done here. - Default: "http", - }, - - "token": { - Type: framework.TypeString, - Description: "Token for API calls", - }, - - "ca_cert": { - Type: framework.TypeString, - Description: `CA certificate to use when verifying Consul server certificate, -must be x509 PEM encoded.`, - }, - - "client_cert": { - Type: framework.TypeString, - Description: `Client certificate used for Consul's TLS communication, -must be x509 PEM encoded and if this is set you need to also set client_key.`, - }, - - "client_key": { - Type: framework.TypeString, - Description: `Client key used for Consul's TLS communication, -must be x509 PEM encoded and if this is set you need to also set client_cert.`, - }, - }, - - Operations: map[logical.Operation]framework.OperationHandler{ - logical.ReadOperation: &framework.PathOperation{ - Callback: b.pathConfigAccessRead, - DisplayAttrs: &framework.DisplayAttributes{ - OperationSuffix: "access-configuration", - }, - }, - logical.UpdateOperation: &framework.PathOperation{ - Callback: b.pathConfigAccessWrite, - DisplayAttrs: &framework.DisplayAttributes{ - OperationVerb: "configure", - OperationSuffix: "access", - }, - }, - }, - } -} - -func (b *backend) readConfigAccess(ctx context.Context, storage logical.Storage) (*accessConfig, error, error) { - entry, err := storage.Get(ctx, "config/access") - if err != nil { - return nil, nil, err - } - if entry == nil { - return nil, fmt.Errorf("access credentials for the backend itself haven't been configured; please configure them at the '/config/access' endpoint"), nil - } - - conf := &accessConfig{} - if err := entry.DecodeJSON(conf); err != nil { - return nil, nil, fmt.Errorf("error reading consul access configuration: %w", err) - } - - return conf, nil, nil -} - -func (b *backend) pathConfigAccessRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - conf, userErr, intErr := b.readConfigAccess(ctx, req.Storage) - if intErr != nil { - return nil, intErr - } - if userErr != nil { - return logical.ErrorResponse(userErr.Error()), nil - } - if conf == nil { - return nil, fmt.Errorf("no user error reported but consul access configuration not found") - } - - return &logical.Response{ - Data: map[string]interface{}{ - "address": conf.Address, - "scheme": conf.Scheme, - }, - }, nil -} - -func (b *backend) pathConfigAccessWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - config := accessConfig{ - Address: data.Get("address").(string), - Scheme: data.Get("scheme").(string), - Token: data.Get("token").(string), - CACert: data.Get("ca_cert").(string), - ClientCert: data.Get("client_cert").(string), - ClientKey: data.Get("client_key").(string), - } - - // If a token has not been given by the user, we try to boostrap the ACL - // support - if config.Token == "" { - consulConf := config.NewConfig() - client, err := api.NewClient(consulConf) - if err != nil { - return nil, err - } - token, _, err := client.ACL().Bootstrap() - if err != nil { - return logical.ErrorResponse("Token not provided and failed to bootstrap ACLs: %s", err), nil - } - config.Token = token.SecretID - } - - entry, err := logical.StorageEntryJSON("config/access", config) - if err != nil { - return nil, err - } - - if err := req.Storage.Put(ctx, entry); err != nil { - return nil, err - } - - return nil, nil -} - -type accessConfig struct { - Address string `json:"address"` - Scheme string `json:"scheme"` - Token string `json:"token"` - CACert string `json:"ca_cert"` - ClientCert string `json:"client_cert"` - ClientKey string `json:"client_key"` -} - -func (conf *accessConfig) NewConfig() *api.Config { - consulConf := api.DefaultNonPooledConfig() - consulConf.Address = conf.Address - consulConf.Scheme = conf.Scheme - consulConf.Token = conf.Token - consulConf.TLSConfig.CAPem = []byte(conf.CACert) - consulConf.TLSConfig.CertPEM = []byte(conf.ClientCert) - consulConf.TLSConfig.KeyPEM = []byte(conf.ClientKey) - - return consulConf -} diff --git a/builtin/logical/consul/path_roles.go b/builtin/logical/consul/path_roles.go deleted file mode 100644 index 1341544ea..000000000 --- a/builtin/logical/consul/path_roles.go +++ /dev/null @@ -1,294 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package consul - -import ( - "context" - "encoding/base64" - "fmt" - "time" - - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/logical" -) - -func pathListRoles(b *backend) *framework.Path { - return &framework.Path{ - Pattern: "roles/?$", - - DisplayAttrs: &framework.DisplayAttributes{ - OperationPrefix: operationPrefixConsul, - OperationSuffix: "roles", - }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ListOperation: b.pathRoleList, - }, - } -} - -func pathRoles(b *backend) *framework.Path { - return &framework.Path{ - Pattern: "roles/" + framework.GenericNameRegex("name"), - - DisplayAttrs: &framework.DisplayAttributes{ - OperationPrefix: operationPrefixConsul, - OperationSuffix: "role", - }, - - Fields: map[string]*framework.FieldSchema{ - "name": { - Type: framework.TypeString, - Description: "Name of the role.", - }, - - // The "policy" and "token_type" parameters were deprecated in Consul back in version 1.4. - // They have been removed from Consul as of version 1.11. Consider removing them here in the future. - "policy": { - Type: framework.TypeString, - Description: `Policy document, base64 encoded. Required -for 'client' tokens. Required for Consul pre-1.4.`, - Deprecated: true, - }, - - "token_type": { - Type: framework.TypeString, - Default: "client", - Description: `Which type of token to create: 'client' or 'management'. If -a 'management' token, the "policy", "policies", and "consul_roles" parameters are not -required. Defaults to 'client'.`, - Deprecated: true, - }, - - "policies": { - Type: framework.TypeCommaStringSlice, - Description: `Use "consul_policies" instead.`, - Deprecated: true, - }, - - "consul_policies": { - Type: framework.TypeCommaStringSlice, - Description: `List of policies to attach to the token. Either "consul_policies" -or "consul_roles" are required for Consul 1.5 and above, or just "consul_policies" if -using Consul 1.4.`, - }, - - "consul_roles": { - Type: framework.TypeCommaStringSlice, - Description: `List of Consul roles to attach to the token. Either "policies" -or "consul_roles" are required for Consul 1.5 and above.`, - }, - - "local": { - Type: framework.TypeBool, - Description: `Indicates that the token should not be replicated globally -and instead be local to the current datacenter. Available in Consul 1.4 and above.`, - }, - - "ttl": { - Type: framework.TypeDurationSecond, - Description: "TTL for the Consul token created from the role.", - }, - - "max_ttl": { - Type: framework.TypeDurationSecond, - Description: "Max TTL for the Consul token created from the role.", - }, - - "lease": { - Type: framework.TypeDurationSecond, - Description: `Use "ttl" instead.`, - Deprecated: true, - }, - - "consul_namespace": { - Type: framework.TypeString, - Description: `Indicates which namespace that the token will be -created within. Defaults to 'default'. Available in Consul 1.7 and above.`, - }, - - "partition": { - Type: framework.TypeString, - Description: `Indicates which admin partition that the token -will be created within. Defaults to 'default'. Available in Consul 1.11 and above.`, - }, - - "service_identities": { - Type: framework.TypeStringSlice, - Description: `List of Service Identities to attach to the -token, separated by semicolons. Available in Consul 1.5 or above.`, - }, - - "node_identities": { - Type: framework.TypeStringSlice, - Description: `List of Node Identities to attach to the -token. Available in Consul 1.8.1 or above.`, - }, - }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.pathRolesRead, - logical.UpdateOperation: b.pathRolesWrite, - logical.DeleteOperation: b.pathRolesDelete, - }, - } -} - -func (b *backend) pathRoleList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - entries, err := req.Storage.List(ctx, "policy/") - if err != nil { - return nil, err - } - - return logical.ListResponse(entries), nil -} - -func (b *backend) pathRolesRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - name := d.Get("name").(string) - - entry, err := req.Storage.Get(ctx, "policy/"+name) - if err != nil { - return nil, err - } - if entry == nil { - return nil, nil - } - - var roleConfigData roleConfig - if err := entry.DecodeJSON(&roleConfigData); err != nil { - return nil, err - } - - if roleConfigData.TokenType == "" { - roleConfigData.TokenType = "client" - } - - // Generate the response - resp := &logical.Response{ - Data: map[string]interface{}{ - "lease": int64(roleConfigData.TTL.Seconds()), - "ttl": int64(roleConfigData.TTL.Seconds()), - "max_ttl": int64(roleConfigData.MaxTTL.Seconds()), - "token_type": roleConfigData.TokenType, - "local": roleConfigData.Local, - "consul_namespace": roleConfigData.ConsulNamespace, - "partition": roleConfigData.Partition, - }, - } - if roleConfigData.Policy != "" { - resp.Data["policy"] = base64.StdEncoding.EncodeToString([]byte(roleConfigData.Policy)) - } - if len(roleConfigData.Policies) > 0 { - resp.Data["consul_policies"] = roleConfigData.Policies - } - if len(roleConfigData.ConsulRoles) > 0 { - resp.Data["consul_roles"] = roleConfigData.ConsulRoles - } - if len(roleConfigData.ServiceIdentities) > 0 { - resp.Data["service_identities"] = roleConfigData.ServiceIdentities - } - if len(roleConfigData.NodeIdentities) > 0 { - resp.Data["node_identities"] = roleConfigData.NodeIdentities - } - - return resp, nil -} - -func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - tokenType := d.Get("token_type").(string) - policy := d.Get("policy").(string) - consulPolicies := d.Get("consul_policies").([]string) - policies := d.Get("policies").([]string) - roles := d.Get("consul_roles").([]string) - serviceIdentities := d.Get("service_identities").([]string) - nodeIdentities := d.Get("node_identities").([]string) - - switch tokenType { - case "client": - if policy == "" && len(policies) == 0 && len(consulPolicies) == 0 && - len(roles) == 0 && len(serviceIdentities) == 0 && len(nodeIdentities) == 0 { - return logical.ErrorResponse( - "Use either a policy document, a list of policies or roles, or a set of service or node identities, depending on your Consul version"), nil - } - case "management": - default: - return logical.ErrorResponse("token_type must be \"client\" or \"management\""), nil - } - - if len(consulPolicies) == 0 { - consulPolicies = policies - } - - policyRaw, err := base64.StdEncoding.DecodeString(policy) - if err != nil { - return logical.ErrorResponse(fmt.Sprintf( - "Error decoding policy base64: %s", err)), nil - } - - var ttl time.Duration - ttlRaw, ok := d.GetOk("ttl") - if ok { - ttl = time.Second * time.Duration(ttlRaw.(int)) - } else { - leaseParamRaw, ok := d.GetOk("lease") - if ok { - ttl = time.Second * time.Duration(leaseParamRaw.(int)) - } - } - - var maxTTL time.Duration - maxTTLRaw, ok := d.GetOk("max_ttl") - if ok { - maxTTL = time.Second * time.Duration(maxTTLRaw.(int)) - } - - name := d.Get("name").(string) - local := d.Get("local").(bool) - namespace := d.Get("consul_namespace").(string) - partition := d.Get("partition").(string) - entry, err := logical.StorageEntryJSON("policy/"+name, roleConfig{ - Policy: string(policyRaw), - Policies: consulPolicies, - ConsulRoles: roles, - ServiceIdentities: serviceIdentities, - NodeIdentities: nodeIdentities, - TokenType: tokenType, - TTL: ttl, - MaxTTL: maxTTL, - Local: local, - ConsulNamespace: namespace, - Partition: partition, - }) - if err != nil { - return nil, err - } - - if err := req.Storage.Put(ctx, entry); err != nil { - return nil, err - } - - return nil, nil -} - -func (b *backend) pathRolesDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - name := d.Get("name").(string) - if err := req.Storage.Delete(ctx, "policy/"+name); err != nil { - return nil, err - } - return nil, nil -} - -type roleConfig struct { - Policy string `json:"policy"` - Policies []string `json:"policies"` - ConsulRoles []string `json:"consul_roles"` - ServiceIdentities []string `json:"service_identities"` - NodeIdentities []string `json:"node_identities"` - TTL time.Duration `json:"lease"` - MaxTTL time.Duration `json:"max_ttl"` - TokenType string `json:"token_type"` - Local bool `json:"local"` - ConsulNamespace string `json:"consul_namespace"` - Partition string `json:"partition"` -} diff --git a/builtin/logical/consul/path_token.go b/builtin/logical/consul/path_token.go deleted file mode 100644 index 6cddd1fdd..000000000 --- a/builtin/logical/consul/path_token.go +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package consul - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/hashicorp/consul/api" - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/logical" -) - -const ( - tokenPolicyType = "token" -) - -func pathToken(b *backend) *framework.Path { - return &framework.Path{ - Pattern: "creds/" + framework.GenericNameRegex("role"), - - DisplayAttrs: &framework.DisplayAttributes{ - OperationPrefix: operationPrefixConsul, - OperationVerb: "generate", - OperationSuffix: "credentials", - }, - - Fields: map[string]*framework.FieldSchema{ - "role": { - Type: framework.TypeString, - Description: "Name of the role.", - }, - }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.pathTokenRead, - }, - } -} - -func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - role := d.Get("role").(string) - entry, err := req.Storage.Get(ctx, "policy/"+role) - if err != nil { - return nil, fmt.Errorf("error retrieving role: %w", err) - } - if entry == nil { - return logical.ErrorResponse(fmt.Sprintf("role %q not found", role)), nil - } - - var roleConfigData roleConfig - if err := entry.DecodeJSON(&roleConfigData); err != nil { - return nil, err - } - - if roleConfigData.TokenType == "" { - roleConfigData.TokenType = "client" - } - - // Get the consul client - c, userErr, intErr := b.client(ctx, req.Storage) - if intErr != nil { - return nil, intErr - } - if userErr != nil { - return logical.ErrorResponse(userErr.Error()), nil - } - - // Generate a name for the token - tokenName := fmt.Sprintf("Vault %s %s %d", role, req.DisplayName, time.Now().UnixNano()) - - writeOpts := &api.WriteOptions{} - writeOpts = writeOpts.WithContext(ctx) - - // Create an ACLEntry for Consul pre 1.4 - if (roleConfigData.Policy != "" && roleConfigData.TokenType == "client") || - (roleConfigData.Policy == "" && roleConfigData.TokenType == "management") { - token, _, err := c.ACL().Create(&api.ACLEntry{ - Name: tokenName, - Type: roleConfigData.TokenType, - Rules: roleConfigData.Policy, - }, writeOpts) - if err != nil { - return logical.ErrorResponse(err.Error()), nil - } - - // Use the helper to create the secret - s := b.Secret(SecretTokenType).Response(map[string]interface{}{ - "token": token, - }, map[string]interface{}{ - "token": token, - "role": role, - }) - s.Secret.TTL = roleConfigData.TTL - s.Secret.MaxTTL = roleConfigData.MaxTTL - return s, nil - } - - // Create an ACLToken for Consul 1.4 and above - policyLinks := []*api.ACLTokenPolicyLink{} - for _, policyName := range roleConfigData.Policies { - policyLinks = append(policyLinks, &api.ACLTokenPolicyLink{ - Name: policyName, - }) - } - - roleLinks := []*api.ACLTokenRoleLink{} - for _, roleName := range roleConfigData.ConsulRoles { - roleLinks = append(roleLinks, &api.ACLTokenRoleLink{ - Name: roleName, - }) - } - - aclServiceIdentities := parseServiceIdentities(roleConfigData.ServiceIdentities) - aclNodeIdentities := parseNodeIdentities(roleConfigData.NodeIdentities) - - token, _, err := c.ACL().TokenCreate(&api.ACLToken{ - Description: tokenName, - Policies: policyLinks, - Roles: roleLinks, - ServiceIdentities: aclServiceIdentities, - NodeIdentities: aclNodeIdentities, - Local: roleConfigData.Local, - Namespace: roleConfigData.ConsulNamespace, - Partition: roleConfigData.Partition, - }, writeOpts) - if err != nil { - return logical.ErrorResponse(err.Error()), nil - } - - // Use the helper to create the secret - s := b.Secret(SecretTokenType).Response(map[string]interface{}{ - "token": token.SecretID, - "accessor": token.AccessorID, - "local": token.Local, - "consul_namespace": token.Namespace, - "partition": token.Partition, - }, map[string]interface{}{ - "token": token.AccessorID, - "role": role, - "version": tokenPolicyType, - }) - s.Secret.TTL = roleConfigData.TTL - s.Secret.MaxTTL = roleConfigData.MaxTTL - - return s, nil -} - -func parseServiceIdentities(data []string) []*api.ACLServiceIdentity { - aclServiceIdentities := []*api.ACLServiceIdentity{} - - for _, serviceIdentity := range data { - entry := &api.ACLServiceIdentity{} - components := strings.Split(serviceIdentity, ":") - entry.ServiceName = components[0] - if len(components) == 2 { - entry.Datacenters = strings.Split(components[1], ",") - } - aclServiceIdentities = append(aclServiceIdentities, entry) - } - - return aclServiceIdentities -} - -func parseNodeIdentities(data []string) []*api.ACLNodeIdentity { - aclNodeIdentities := []*api.ACLNodeIdentity{} - - for _, nodeIdentity := range data { - entry := &api.ACLNodeIdentity{} - components := strings.Split(nodeIdentity, ":") - entry.NodeName = components[0] - if len(components) > 1 { - entry.Datacenter = components[1] - } - aclNodeIdentities = append(aclNodeIdentities, entry) - } - - return aclNodeIdentities -} diff --git a/builtin/logical/consul/path_token_test.go b/builtin/logical/consul/path_token_test.go deleted file mode 100644 index 7f5ac3d2b..000000000 --- a/builtin/logical/consul/path_token_test.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package consul - -import ( - "reflect" - "testing" - - "github.com/hashicorp/consul/api" -) - -func TestToken_parseServiceIdentities(t *testing.T) { - tests := []struct { - name string - args []string - want []*api.ACLServiceIdentity - }{ - { - name: "No datacenters", - args: []string{"myservice-1"}, - want: []*api.ACLServiceIdentity{{ServiceName: "myservice-1", Datacenters: nil}}, - }, - { - name: "One datacenter", - args: []string{"myservice-1:dc1"}, - want: []*api.ACLServiceIdentity{{ServiceName: "myservice-1", Datacenters: []string{"dc1"}}}, - }, - { - name: "Multiple datacenters", - args: []string{"myservice-1:dc1,dc2,dc3"}, - want: []*api.ACLServiceIdentity{{ServiceName: "myservice-1", Datacenters: []string{"dc1", "dc2", "dc3"}}}, - }, - { - name: "Missing service name with datacenter", - args: []string{":dc1"}, - want: []*api.ACLServiceIdentity{{ServiceName: "", Datacenters: []string{"dc1"}}}, - }, - { - name: "Missing service name and missing datacenter", - args: []string{""}, - want: []*api.ACLServiceIdentity{{ServiceName: "", Datacenters: nil}}, - }, - { - name: "Multiple service identities", - args: []string{"myservice-1:dc1", "myservice-2:dc1", "myservice-3:dc1,dc2"}, - want: []*api.ACLServiceIdentity{ - {ServiceName: "myservice-1", Datacenters: []string{"dc1"}}, - {ServiceName: "myservice-2", Datacenters: []string{"dc1"}}, - {ServiceName: "myservice-3", Datacenters: []string{"dc1", "dc2"}}, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := parseServiceIdentities(tt.args); !reflect.DeepEqual(got, tt.want) { - t.Errorf("parseServiceIdentities() = {%s:%v}, want {%s:%v}", got[0].ServiceName, got[0].Datacenters, tt.want[0].ServiceName, tt.want[0].Datacenters) - } - }) - } -} - -func TestToken_parseNodeIdentities(t *testing.T) { - tests := []struct { - name string - args []string - want []*api.ACLNodeIdentity - }{ - { - name: "No datacenter", - args: []string{"server-1"}, - want: []*api.ACLNodeIdentity{{NodeName: "server-1", Datacenter: ""}}, - }, - { - name: "One datacenter", - args: []string{"server-1:dc1"}, - want: []*api.ACLNodeIdentity{{NodeName: "server-1", Datacenter: "dc1"}}, - }, - { - name: "Missing node name with datacenter", - args: []string{":dc1"}, - want: []*api.ACLNodeIdentity{{NodeName: "", Datacenter: "dc1"}}, - }, - { - name: "Missing node name and missing datacenter", - args: []string{""}, - want: []*api.ACLNodeIdentity{{NodeName: "", Datacenter: ""}}, - }, - { - name: "Multiple node identities", - args: []string{"server-1:dc1", "server-2:dc1", "server-3:dc1"}, - want: []*api.ACLNodeIdentity{ - {NodeName: "server-1", Datacenter: "dc1"}, - {NodeName: "server-2", Datacenter: "dc1"}, - {NodeName: "server-3", Datacenter: "dc1"}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := parseNodeIdentities(tt.args); !reflect.DeepEqual(got, tt.want) { - t.Errorf("parseNodeIdentities() = {%s:%s}, want {%s:%s}", got[0].NodeName, got[0].Datacenter, tt.want[0].NodeName, tt.want[0].Datacenter) - } - }) - } -} diff --git a/builtin/logical/consul/secret_token.go b/builtin/logical/consul/secret_token.go deleted file mode 100644 index f2219f079..000000000 --- a/builtin/logical/consul/secret_token.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package consul - -import ( - "context" - "fmt" - - "github.com/hashicorp/consul/api" - "github.com/hashicorp/vault/sdk/framework" - "github.com/hashicorp/vault/sdk/logical" -) - -const ( - SecretTokenType = "token" -) - -func secretToken(b *backend) *framework.Secret { - return &framework.Secret{ - Type: SecretTokenType, - Fields: map[string]*framework.FieldSchema{ - "token": { - Type: framework.TypeString, - Description: "Request token", - }, - }, - - Renew: b.secretTokenRenew, - Revoke: b.secretTokenRevoke, - } -} - -func (b *backend) secretTokenRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - resp := &logical.Response{Secret: req.Secret} - roleRaw, ok := req.Secret.InternalData["role"] - if !ok { - return resp, nil - } - - role, ok := roleRaw.(string) - if !ok { - return resp, nil - } - - entry, err := req.Storage.Get(ctx, "policy/"+role) - if err != nil { - return nil, fmt.Errorf("error retrieving role: %w", err) - } - if entry == nil { - return logical.ErrorResponse(fmt.Sprintf("issuing role %q not found", role)), nil - } - - var result roleConfig - if err := entry.DecodeJSON(&result); err != nil { - return nil, err - } - resp.Secret.TTL = result.TTL - resp.Secret.MaxTTL = result.MaxTTL - return resp, nil -} - -func (b *backend) secretTokenRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - c, userErr, intErr := b.client(ctx, req.Storage) - if intErr != nil { - return nil, intErr - } - if userErr != nil { - // Returning logical.ErrorResponse from revocation function is risky - return nil, userErr - } - - tokenRaw, ok := req.Secret.InternalData["token"] - if !ok { - // We return nil here because this is a pre-0.5.3 problem and there is - // nothing we can do about it. We already can't revoke the lease - // properly if it has been renewed and this is documented pre-0.5.3 - // behavior with a security bulletin about it. - return nil, nil - } - - var version string - versionRaw, ok := req.Secret.InternalData["version"] - if ok { - version = versionRaw.(string) - } - - // Extract Consul Namespace and Partition info from secret - var revokeWriteOptions *api.WriteOptions - var namespace, partition string - - namespaceRaw, ok := req.Data["consul_namespace"] - if ok { - namespace = namespaceRaw.(string) - } - partitionRaw, ok := req.Data["partition"] - if ok { - partition = partitionRaw.(string) - } - - revokeWriteOptions = &api.WriteOptions{ - Namespace: namespace, - Partition: partition, - } - - switch version { - case "": - // Pre 1.4 tokens - _, err := c.ACL().Destroy(tokenRaw.(string), nil) - if err != nil { - return nil, err - } - case tokenPolicyType: - _, err := c.ACL().TokenDelete(tokenRaw.(string), revokeWriteOptions) - if err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("Invalid version string in data: %s", version) - } - - return nil, nil -} diff --git a/command/base_predict.go b/command/base_predict.go index 8ab926978..ad54526bf 100644 --- a/command/base_predict.go +++ b/command/base_predict.go @@ -85,7 +85,6 @@ func (b *BaseCommand) PredictVaultAvailableMounts() complete.Predictor { // This list does not contain deprecated backends. At present, there is no // API that lists all available secret backends, so this is hard-coded :(. return complete.PredictSet( - "consul", "database", "generic", "pki", diff --git a/command/commands.go b/command/commands.go index 4eeeea942..dfd2b5352 100644 --- a/command/commands.go +++ b/command/commands.go @@ -40,7 +40,6 @@ import ( logicalKv "github.com/hashicorp/vault-plugin-secrets-kv" logicalDb "github.com/hashicorp/vault/builtin/logical/database" - physConsul "github.com/hashicorp/vault/physical/consul" physRaft "github.com/hashicorp/vault/physical/raft" physFile "github.com/hashicorp/vault/sdk/physical/file" physInmem "github.com/hashicorp/vault/sdk/physical/inmem" @@ -162,7 +161,6 @@ var ( } physicalBackends = map[string]physical.Factory{ - "consul": physConsul.NewConsulBackend, "file_transactional": physFile.NewTransactionalFileBackend, "file": physFile.NewFileBackend, "inmem_ha": physInmem.NewInmemHA, diff --git a/command/operator_diagnose.go b/command/operator_diagnose.go index d64e4b2b1..efa94a7da 100644 --- a/command/operator_diagnose.go +++ b/command/operator_diagnose.go @@ -27,7 +27,6 @@ import ( "github.com/hashicorp/vault/helper/metricsutil" "github.com/hashicorp/vault/internalshared/configutil" "github.com/hashicorp/vault/internalshared/listenerutil" - physconsul "github.com/hashicorp/vault/physical/consul" "github.com/hashicorp/vault/physical/raft" "github.com/hashicorp/vault/sdk/physical" sr "github.com/hashicorp/vault/serviceregistration" @@ -317,28 +316,6 @@ func (c *OperatorDiagnoseCommand) offlineDiagnostics(ctx context.Context) error diagnose.RaftStorageQuorum(ctx, (*backend).(*raft.RaftBackend)) } - // Consul storage checks - if config.Storage != nil && config.Storage.Type == storageTypeConsul { - diagnose.Test(ctx, "Check Consul TLS", func(ctx context.Context) error { - err := physconsul.SetupSecureTLS(ctx, api.DefaultConfig(), config.Storage.Config, server.logger, true) - if err != nil { - return err - } - return nil - }) - - diagnose.Test(ctx, "Check Consul Direct Storage Access", func(ctx context.Context) error { - dirAccess := diagnose.ConsulDirectAccess(config.Storage.Config) - if dirAccess != "" { - diagnose.Warn(ctx, dirAccess) - } - if dirAccess == diagnose.DirAccessErr { - diagnose.Advise(ctx, diagnose.DirAccessAdvice) - } - return nil - }) - } - // Attempt to use storage backend if !c.skipEndEnd && config.Storage.Type != storageTypeRaft { diagnose.Test(ctx, "Check Storage Access", diagnose.WithTimeout(30*time.Second, func(ctx context.Context) error { @@ -552,15 +529,6 @@ SEALFAIL: } return nil }) - if config.HAStorage != nil && config.HAStorage.Type == storageTypeConsul { - diagnose.Test(ctx, "Check Consul TLS", func(ctx context.Context) error { - err = physconsul.SetupSecureTLS(ctx, api.DefaultConfig(), config.HAStorage.Config, server.logger, true) - if err != nil { - return err - } - return nil - }) - } return nil }) diff --git a/command/server.go b/command/server.go index e2ed54c1a..c9a104502 100644 --- a/command/server.go +++ b/command/server.go @@ -86,8 +86,7 @@ const ( // Even though there are more types than the ones below, the following consts // are declared internally for value comparison and reusability. - storageTypeRaft = "raft" - storageTypeConsul = "consul" + storageTypeRaft = "raft" ) type ServerCommand struct { @@ -145,7 +144,6 @@ type ServerCommand struct { flagDevClusterJson string flagTestVerifyOnly bool flagTestServerConfig bool - flagDevConsul bool flagExitOnCoreShutdown bool } @@ -367,13 +365,6 @@ func (c *ServerCommand) Flags() *FlagSets { Hidden: true, }) - f.BoolVar(&BoolVar{ - Name: "dev-consul", - Target: &c.flagDevConsul, - Default: false, - Hidden: true, - }) - f.StringVar(&StringVar{ Name: "dev-cluster-json", Target: &c.flagDevClusterJson, @@ -789,16 +780,6 @@ func (c *ServerCommand) setupStorage(config *server.Config) (physical.Backend, e // Do any custom configuration needed per backend switch config.Storage.Type { - case storageTypeConsul: - if config.ServiceRegistration == nil { - // If Consul is configured for storage and service registration is unconfigured, - // use Consul for service registration without requiring additional configuration. - // This maintains backward-compatibility. - config.ServiceRegistration = &server.ServiceRegistration{ - Type: "consul", - Config: config.Storage.Config, - } - } case storageTypeRaft: if envCA := os.Getenv("VAULT_CLUSTER_ADDR"); envCA != "" { config.ClusterAddr = envCA @@ -935,8 +916,6 @@ func configureDevTLS(c *ServerCommand) (func(), *server.Config, string, error) { var devStorageType string switch { - case c.flagDevConsul: - devStorageType = "consul" case c.flagDevHA && c.flagDevTransactional: devStorageType = "inmem_transactional_ha" case !c.flagDevHA && c.flagDevTransactional: @@ -1017,7 +996,7 @@ func (c *ServerCommand) Run(args []string) int { } // Automatically enable dev mode if other dev flags are provided. - if c.flagDevConsul || c.flagDevHA || c.flagDevTransactional || c.flagDevLeasedKV || c.flagDevThreeNode || c.flagDevFourCluster || c.flagDevAutoSeal || c.flagDevKVV1 || c.flagDevTLS { + if c.flagDevHA || c.flagDevTransactional || c.flagDevLeasedKV || c.flagDevThreeNode || c.flagDevFourCluster || c.flagDevAutoSeal || c.flagDevKVV1 || c.flagDevTLS { c.flagDev = true } @@ -1319,7 +1298,7 @@ func (c *ServerCommand) Run(args []string) int { if !storageSupportedForEnt(&coreConfig) { c.UI.Warn("") - c.UI.Warn(wrapAtLength(fmt.Sprintf("WARNING: storage configured to use %q which is not supported for Vault Enterprise, must be \"raft\" or \"consul\"", coreConfig.StorageType))) + c.UI.Warn(wrapAtLength(fmt.Sprintf("WARNING: storage configured to use %q which is not supported for Vault Enterprise, must be \"raft\"", coreConfig.StorageType))) c.UI.Warn("") } diff --git a/helper/builtinplugins/registry.go b/helper/builtinplugins/registry.go index d87d7cfb2..2a79701a6 100644 --- a/helper/builtinplugins/registry.go +++ b/helper/builtinplugins/registry.go @@ -23,7 +23,6 @@ import ( credOkta "github.com/hashicorp/vault/builtin/credential/okta" credRadius "github.com/hashicorp/vault/builtin/credential/radius" credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" - logicalConsul "github.com/hashicorp/vault/builtin/logical/consul" logicalNomad "github.com/hashicorp/vault/builtin/logical/nomad" logicalPki "github.com/hashicorp/vault/builtin/logical/pki" logicalRabbit "github.com/hashicorp/vault/builtin/logical/rabbitmq" @@ -121,7 +120,6 @@ func newRegistry() *registry { Factory: removedFactory, DeprecationStatus: consts.Removed, }, - "consul": {Factory: logicalConsul.Factory}, "kubernetes": {Factory: logicalKube.Factory}, "kv": {Factory: logicalKv.Factory}, "mysql": { diff --git a/physical/consul/consul.go b/physical/consul/consul.go deleted file mode 100644 index d7c5e6505..000000000 --- a/physical/consul/consul.go +++ /dev/null @@ -1,784 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package consul - -import ( - "context" - "errors" - "fmt" - "net/http" - "strconv" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/armon/go-metrics" - "github.com/hashicorp/consul/api" - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/go-multierror" - "github.com/hashicorp/go-secure-stdlib/parseutil" - "github.com/hashicorp/go-secure-stdlib/tlsutil" - "github.com/hashicorp/vault/sdk/helper/consts" - "github.com/hashicorp/vault/sdk/physical" - "github.com/hashicorp/vault/vault/diagnose" - "golang.org/x/net/http2" -) - -const ( - // consistencyModeDefault is the configuration value used to tell - // consul to use default consistency. - consistencyModeDefault = "default" - - // consistencyModeStrong is the configuration value used to tell - // consul to use strong consistency. - consistencyModeStrong = "strong" - - // nonExistentKey is used as part of a capabilities check against Consul - nonExistentKey = "F35C28E1-7035-40BB-B865-6BED9E3A1B28" -) - -// Verify ConsulBackend satisfies the correct interfaces -var ( - _ physical.Backend = (*ConsulBackend)(nil) - _ physical.FencingHABackend = (*ConsulBackend)(nil) - _ physical.Lock = (*ConsulLock)(nil) - _ physical.Transactional = (*ConsulBackend)(nil) - - GetInTxnDisabledError = errors.New("get operations inside transactions are disabled in consul backend") -) - -// ConsulBackend is a physical backend that stores data at specific -// prefix within Consul. It is used for most production situations as -// it allows Vault to run on multiple machines in a highly-available manner. -// failGetInTxn is only used in tests. -type ConsulBackend struct { - logger log.Logger - client *api.Client - path string - kv *api.KV - txn *api.Txn - permitPool *physical.PermitPool - consistencyMode string - sessionTTL string - lockWaitTime time.Duration - failGetInTxn *uint32 - activeNodeLock atomic.Pointer[ConsulLock] -} - -// NewConsulBackend constructs a Consul backend using the given API client -// and the prefix in the KV store. -func NewConsulBackend(conf map[string]string, logger log.Logger) (physical.Backend, error) { - // Get the path in Consul - path, ok := conf["path"] - if !ok { - path = "vault/" - } - if logger.IsDebug() { - logger.Debug("config path set", "path", path) - } - - // Ensure path is suffixed but not prefixed - if !strings.HasSuffix(path, "/") { - logger.Warn("appending trailing forward slash to path") - path += "/" - } - if strings.HasPrefix(path, "/") { - logger.Warn("trimming path of its forward slash") - path = strings.TrimPrefix(path, "/") - } - - sessionTTL := api.DefaultLockSessionTTL - sessionTTLStr, ok := conf["session_ttl"] - if ok { - _, err := parseutil.ParseDurationSecond(sessionTTLStr) - if err != nil { - return nil, fmt.Errorf("invalid session_ttl: %w", err) - } - sessionTTL = sessionTTLStr - if logger.IsDebug() { - logger.Debug("config session_ttl set", "session_ttl", sessionTTL) - } - } - - lockWaitTime := api.DefaultLockWaitTime - lockWaitTimeRaw, ok := conf["lock_wait_time"] - if ok { - d, err := parseutil.ParseDurationSecond(lockWaitTimeRaw) - if err != nil { - return nil, fmt.Errorf("invalid lock_wait_time: %w", err) - } - lockWaitTime = d - if logger.IsDebug() { - logger.Debug("config lock_wait_time set", "lock_wait_time", d) - } - } - - maxParStr, ok := conf["max_parallel"] - var maxParInt int - if ok { - maxParInt, err := strconv.Atoi(maxParStr) - if err != nil { - return nil, fmt.Errorf("failed parsing max_parallel parameter: %w", err) - } - if logger.IsDebug() { - logger.Debug("max_parallel set", "max_parallel", maxParInt) - } - } - - consistencyMode, ok := conf["consistency_mode"] - if ok { - switch consistencyMode { - case consistencyModeDefault, consistencyModeStrong: - default: - return nil, fmt.Errorf("invalid consistency_mode value: %q", consistencyMode) - } - } else { - consistencyMode = consistencyModeDefault - } - - // Configure the client - consulConf := api.DefaultConfig() - // Set MaxIdleConnsPerHost to the number of processes used in expiration.Restore - consulConf.Transport.MaxIdleConnsPerHost = consts.ExpirationRestoreWorkerCount - - if err := SetupSecureTLS(context.Background(), consulConf, conf, logger, false); err != nil { - return nil, fmt.Errorf("client setup failed: %w", err) - } - - consulConf.HttpClient = &http.Client{Transport: consulConf.Transport} - client, err := api.NewClient(consulConf) - if err != nil { - return nil, fmt.Errorf("client setup failed: %w", err) - } - - // Set up the backend - c := &ConsulBackend{ - logger: logger, - path: path, - client: client, - kv: client.KV(), - txn: client.Txn(), - permitPool: physical.NewPermitPool(maxParInt), - consistencyMode: consistencyMode, - sessionTTL: sessionTTL, - lockWaitTime: lockWaitTime, - failGetInTxn: new(uint32), - } - - return c, nil -} - -func SetupSecureTLS(ctx context.Context, consulConf *api.Config, conf map[string]string, logger log.Logger, isDiagnose bool) error { - if addr, ok := conf["address"]; ok { - consulConf.Address = addr - if logger.IsDebug() { - logger.Debug("config address set", "address", addr) - } - - // Copied from the Consul API module; set the Scheme based on - // the protocol field if address looks ike a URL. - // This can enable the TLS configuration below. - parts := strings.SplitN(addr, "://", 2) - if len(parts) == 2 { - if parts[0] == "http" || parts[0] == "https" { - consulConf.Scheme = parts[0] - consulConf.Address = parts[1] - if logger.IsDebug() { - logger.Debug("config address parsed", "scheme", parts[0]) - logger.Debug("config scheme parsed", "address", parts[1]) - } - } // allow "unix:" or whatever else consul supports in the future - } - } - if scheme, ok := conf["scheme"]; ok { - consulConf.Scheme = scheme - if logger.IsDebug() { - logger.Debug("config scheme set", "scheme", scheme) - } - } - if token, ok := conf["token"]; ok { - consulConf.Token = token - logger.Debug("config token set") - } - - if consulConf.Scheme == "https" { - if isDiagnose { - certPath, okCert := conf["tls_cert_file"] - keyPath, okKey := conf["tls_key_file"] - if okCert && okKey { - warnings, err := diagnose.TLSFileChecks(certPath, keyPath) - for _, warning := range warnings { - diagnose.Warn(ctx, warning) - } - if err != nil { - return err - } - return nil - } - return fmt.Errorf("key or cert path: %s, %s, cannot be loaded from consul config file", certPath, keyPath) - } - - // Use the parsed Address instead of the raw conf['address'] - tlsClientConfig, err := tlsutil.SetupTLSConfig(conf, consulConf.Address) - if err != nil { - return err - } - - consulConf.Transport.TLSClientConfig = tlsClientConfig - if err := http2.ConfigureTransport(consulConf.Transport); err != nil { - return err - } - logger.Debug("configured TLS") - } else { - if isDiagnose { - diagnose.Skipped(ctx, "HTTPS is not used, Skipping TLS verification.") - } - } - return nil -} - -// ExpandedCapabilitiesAvailable tests to see if Consul has KVGetOrEmpty and 128 entries per transaction available -func (c *ConsulBackend) ExpandedCapabilitiesAvailable(ctx context.Context) bool { - available := false - - maxEntries := 128 - ops := make([]*api.TxnOp, maxEntries) - for i := 0; i < maxEntries; i++ { - ops[i] = &api.TxnOp{KV: &api.KVTxnOp{ - Key: c.path + nonExistentKey, - Verb: api.KVGetOrEmpty, - }} - } - - c.permitPool.Acquire() - defer c.permitPool.Release() - - queryOpts := &api.QueryOptions{} - queryOpts = queryOpts.WithContext(ctx) - - ok, resp, _, err := c.txn.Txn(ops, queryOpts) - if ok && len(resp.Errors) == 0 && err == nil { - available = true - } - - return available -} - -func (c *ConsulBackend) writeTxnOps(ctx context.Context, len int) ([]*api.TxnOp, string) { - if len < 1 { - len = 1 - } - ops := make([]*api.TxnOp, 0, len+1) - - // If we don't have a lock yet, return a transaction with no session check. We - // need to do this to allow writes during cluster initialization before there - // is an active node. - lock := c.activeNodeLock.Load() - if lock == nil { - return ops, "" - } - - lockKey, lockSession := lock.Info() - if lockKey == "" || lockSession == "" { - return ops, "" - } - - // If the context used to write has been marked as a special case write that - // happens outside of a lock then don't add the session check. - if physical.IsUnfencedWrite(ctx) { - return ops, "" - } - - // Insert the session check operation at index 0. This will allow us later to - // work out easily if a write failure is because of the session check. - ops = append(ops, &api.TxnOp{ - KV: &api.KVTxnOp{ - Verb: api.KVCheckSession, - Key: lockKey, - Session: lockSession, - }, - }) - return ops, lockSession -} - -// Transaction is used to run multiple entries via a transaction. -func (c *ConsulBackend) Transaction(ctx context.Context, txns []*physical.TxnEntry) error { - return c.txnInternal(ctx, txns, "transaction") -} - -func (c *ConsulBackend) txnInternal(ctx context.Context, txns []*physical.TxnEntry, apiOpName string) error { - if len(txns) == 0 { - return nil - } - defer metrics.MeasureSince([]string{"consul", apiOpName}, time.Now()) - - failGetInTxn := atomic.LoadUint32(c.failGetInTxn) - for _, t := range txns { - if t.Operation == physical.GetOperation && failGetInTxn != 0 { - return GetInTxnDisabledError - } - } - - ops, sessionID := c.writeTxnOps(ctx, len(txns)) - for _, t := range txns { - o, err := c.makeApiTxn(t) - if err != nil { - return fmt.Errorf("error converting physical transactions into api transactions: %w", err) - } - - ops = append(ops, o) - } - - c.permitPool.Acquire() - defer c.permitPool.Release() - - var retErr *multierror.Error - kvMap := make(map[string][]byte, 0) - - queryOpts := &api.QueryOptions{} - queryOpts = queryOpts.WithContext(ctx) - - ok, resp, _, err := c.txn.Txn(ops, queryOpts) - if err != nil { - if strings.Contains(err.Error(), "is too large") { - return fmt.Errorf("%s: %w", physical.ErrValueTooLarge, err) - } - return err - } - if ok && len(resp.Errors) == 0 { - // Loop over results and cache them in a map. Note that we're only caching - // the first time we see a key, which _should_ correspond to a Get - // operation, since we expect those come first in our txns slice (though - // after check-session). - for _, txnr := range resp.Results { - if len(txnr.KV.Value) > 0 { - // We need to trim the Consul kv path (typically "vault/") from the key - // otherwise it won't match the transaction entries we have. - key := strings.TrimPrefix(txnr.KV.Key, c.path) - if _, found := kvMap[key]; !found { - kvMap[key] = txnr.KV.Value - } - } - } - } - - if len(resp.Errors) > 0 { - for _, res := range resp.Errors { - retErr = multierror.Append(retErr, errors.New(res.What)) - if res.OpIndex == 0 && sessionID != "" { - // We added a session check (sessionID not empty) so an error at OpIndex - // 0 means that we failed that session check. We don't attempt to string - // match because Consul can return at least three different errors here - // with no common string. In all cases though failing this check means - // we no longer hold the lock because it was released, modified or - // deleted. Rather than just continuing to try writing until the - // blocking query manages to notice we're no longer the lock holder - // (which can take 10s of seconds even in good network conditions in my - // testing) we can now Unlock directly here. Our ConsulLock now has a - // shortcut that will cause the lock to close the leaderCh immediately - // when we call without waiting for the blocking query to return (unlike - // Consul's current Lock implementation). But before we unlock, we - // should re-load the lock and ensure it's still the same instance we - // just tried to write with in case this goroutine is somehow really - // delayed and we actually acquired a whole new lock in the meantime! - lock := c.activeNodeLock.Load() - if lock != nil { - _, lockSessionID := lock.Info() - if sessionID == lockSessionID { - c.logger.Warn("session check failed on write, we lost active node lock, stepping down", "err", res.What) - lock.Unlock() - } - } - } - } - } - - if retErr != nil { - return retErr - } - - // Loop over our get transactions and populate any values found in our map cache. - for _, t := range txns { - if val, ok := kvMap[t.Entry.Key]; ok && t.Operation == physical.GetOperation { - newVal := make([]byte, len(val)) - copy(newVal, val) - t.Entry.Value = newVal - } - } - - return nil -} - -func (c *ConsulBackend) makeApiTxn(txn *physical.TxnEntry) (*api.TxnOp, error) { - op := &api.KVTxnOp{ - Key: c.path + txn.Entry.Key, - } - switch txn.Operation { - case physical.GetOperation: - op.Verb = api.KVGetOrEmpty - case physical.DeleteOperation: - op.Verb = api.KVDelete - case physical.PutOperation: - op.Verb = api.KVSet - op.Value = txn.Entry.Value - default: - return nil, fmt.Errorf("%q is not a supported transaction operation", txn.Operation) - } - - return &api.TxnOp{KV: op}, nil -} - -// Put is used to insert or update an entry -func (c *ConsulBackend) Put(ctx context.Context, entry *physical.Entry) error { - txns := []*physical.TxnEntry{ - { - Operation: physical.PutOperation, - Entry: entry, - }, - } - return c.txnInternal(ctx, txns, "put") -} - -// Get is used to fetch an entry -func (c *ConsulBackend) Get(ctx context.Context, key string) (*physical.Entry, error) { - defer metrics.MeasureSince([]string{"consul", "get"}, time.Now()) - - c.permitPool.Acquire() - defer c.permitPool.Release() - - queryOpts := &api.QueryOptions{} - queryOpts = queryOpts.WithContext(ctx) - - if c.consistencyMode == consistencyModeStrong { - queryOpts.RequireConsistent = true - } - - pair, _, err := c.kv.Get(c.path+key, queryOpts) - if err != nil { - return nil, err - } - if pair == nil { - return nil, nil - } - ent := &physical.Entry{ - Key: key, - Value: pair.Value, - } - return ent, nil -} - -// Delete is used to permanently delete an entry -func (c *ConsulBackend) Delete(ctx context.Context, key string) error { - txns := []*physical.TxnEntry{ - { - Operation: physical.DeleteOperation, - Entry: &physical.Entry{ - Key: key, - }, - }, - } - return c.txnInternal(ctx, txns, "delete") -} - -// List is used to list all the keys under a given -// prefix, up to the next prefix. -func (c *ConsulBackend) List(ctx context.Context, prefix string) ([]string, error) { - defer metrics.MeasureSince([]string{"consul", "list"}, time.Now()) - scan := c.path + prefix - - // The TrimPrefix call below will not work correctly if we have "//" at the - // end. This can happen in cases where you are e.g. listing the root of a - // prefix in a logical backend via "/" instead of "" - if strings.HasSuffix(scan, "//") { - scan = scan[:len(scan)-1] - } - - c.permitPool.Acquire() - defer c.permitPool.Release() - - queryOpts := &api.QueryOptions{} - queryOpts = queryOpts.WithContext(ctx) - - out, _, err := c.kv.Keys(scan, "/", queryOpts) - for idx, val := range out { - out[idx] = strings.TrimPrefix(val, scan) - } - - return out, err -} - -func (c *ConsulBackend) FailGetInTxn(fail bool) { - var val uint32 - if fail { - val = 1 - } - atomic.StoreUint32(c.failGetInTxn, val) -} - -// LockWith is used for mutual exclusion based on the given key. -func (c *ConsulBackend) LockWith(key, value string) (physical.Lock, error) { - cl := &ConsulLock{ - logger: c.logger, - client: c.client, - key: c.path + key, - value: value, - consistencyMode: c.consistencyMode, - sessionTTL: c.sessionTTL, - lockWaitTime: c.lockWaitTime, - } - return cl, nil -} - -// HAEnabled indicates whether the HA functionality should be exposed. -// Currently always returns true. -func (c *ConsulBackend) HAEnabled() bool { - return true -} - -// DetectHostAddr is used to detect the host address by asking the Consul agent -func (c *ConsulBackend) DetectHostAddr() (string, error) { - agent := c.client.Agent() - self, err := agent.Self() - if err != nil { - return "", err - } - addr, ok := self["Member"]["Addr"].(string) - if !ok { - return "", fmt.Errorf("unable to convert an address to string") - } - return addr, nil -} - -// RegisterActiveNodeLock is called after active node lock is obtained to allow -// us to fence future writes. -func (c *ConsulBackend) RegisterActiveNodeLock(l physical.Lock) error { - cl, ok := l.(*ConsulLock) - if !ok { - return fmt.Errorf("invalid Lock type") - } - c.activeNodeLock.Store(cl) - key, sessionID := cl.Info() - c.logger.Info("registered active node lock", "key", key, "sessionID", sessionID) - return nil -} - -// ConsulLock is used to provide the Lock interface backed by Consul. We work -// around some limitations of Consuls api.Lock noted in -// https://github.com/hashicorp/consul/issues/18271 by creating and managing the -// session ourselves, while using Consul's Lock to do the heavy lifting. -type ConsulLock struct { - logger log.Logger - client *api.Client - key string - value string - consistencyMode string - sessionTTL string - lockWaitTime time.Duration - - mu sync.Mutex // protects session state - session *lockSession - // sessionID is a copy of the value from session.id. We use a separate field - // because `Info` needs to keep returning the same sessionID after Unlock has - // cleaned up the session state so that we continue to fence any writes still - // in flight after the lock is Unlocked. It's easier to reason about that as a - // separate field rather than keeping an already-terminated session object - // around. Once Lock is called again this will be replaced (while mu is - // locked) with the new session ID. Must hold mu to read or write this. - sessionID string -} - -type lockSession struct { - // id is immutable after the session is created so does not need mu held - id string - - // mu protects the lock and unlockCh to ensure they are only cleaned up once - mu sync.Mutex - lock *api.Lock - unlockCh chan struct{} -} - -func (s *lockSession) Lock(stopCh <-chan struct{}) (<-chan struct{}, error) { - s.mu.Lock() - defer s.mu.Unlock() - - lockHeld := false - defer func() { - if !lockHeld { - s.cleanupLocked() - } - }() - - consulLeaderCh, err := s.lock.Lock(stopCh) - if err != nil { - return nil, err - } - if consulLeaderCh == nil { - // If both leaderCh and err are nil from Consul's Lock then it means we - // waited for the lockWait without grabbing it. - return nil, nil - } - // We got the Lock, monitor it! - lockHeld = true - leaderCh := make(chan struct{}) - go s.monitorLock(leaderCh, s.unlockCh, consulLeaderCh) - return leaderCh, nil -} - -// monitorLock waits for either unlockCh or consulLeaderCh to close and then -// closes leaderCh. It's designed to be run in a separate goroutine. Note that -// we pass unlockCh rather than accessing it via the member variable because it -// is mutated under the lock during Unlock so reading it from c could be racy. -// We just need the chan created at the call site here so we pass it instead of -// locking and unlocking in here. -func (s *lockSession) monitorLock(leaderCh chan struct{}, unlockCh, consulLeaderCh <-chan struct{}) { - select { - case <-unlockCh: - case <-consulLeaderCh: - } - // We lost the lock. Close the leaderCh - close(leaderCh) - - // Whichever chan closed, cleanup to unwind all the state. If we were - // triggered by a cleanup call this will be a no-op, but if not it ensures all - // state is cleaned up correctly. - s.cleanup() -} - -func (s *lockSession) cleanup() { - s.mu.Lock() - defer s.mu.Unlock() - - s.cleanupLocked() -} - -func (s *lockSession) cleanupLocked() { - if s.lock != nil { - s.lock.Unlock() - s.lock = nil - } - if s.unlockCh != nil { - close(s.unlockCh) - s.unlockCh = nil - } - // Don't bother destroying sessions as they will be destroyed after TTL - // anyway. -} - -func (c *ConsulLock) createSession() (*lockSession, error) { - se := &api.SessionEntry{ - Name: "Vault Lock", - TTL: c.sessionTTL, - // We use Consul's default LockDelay of 15s by not specifying it - } - session, _, err := c.client.Session().Create(se, nil) - if err != nil { - return nil, err - } - - opts := &api.LockOptions{ - Key: c.key, - Value: []byte(c.value), - Session: session, - MonitorRetries: 5, - LockWaitTime: c.lockWaitTime, - SessionTTL: c.sessionTTL, - } - lock, err := c.client.LockOpts(opts) - if err != nil { - // Don't bother destroying sessions as they will be destroyed after TTL - // anyway. - return nil, fmt.Errorf("failed to create lock: %w", err) - } - - unlockCh := make(chan struct{}) - - s := &lockSession{ - id: session, - lock: lock, - unlockCh: unlockCh, - } - - // Start renewals of the session - go func() { - // Note we capture unlockCh here rather than s.unlockCh because s.unlockCh - // is mutated on cleanup which is racy since we don't hold a lock here. - // unlockCh will never be mutated though. - err := c.client.Session().RenewPeriodic(c.sessionTTL, session, nil, unlockCh) - if err != nil { - c.logger.Error("failed to renew consul session for more than the TTL, lock lost", "err", err) - } - // release other resources for this session only i.e. don't c.Unlock as that - // might now be locked under a different session). - s.cleanup() - }() - return s, nil -} - -func (c *ConsulLock) Lock(stopCh <-chan struct{}) (<-chan struct{}, error) { - c.mu.Lock() - defer c.mu.Unlock() - - if c.session != nil { - return nil, fmt.Errorf("lock instance already locked") - } - - session, err := c.createSession() - if err != nil { - return nil, err - } - leaderCh, err := session.Lock(stopCh) - if leaderCh != nil && err == nil { - // We hold the lock, store the session - c.session = session - c.sessionID = session.id - } - return leaderCh, err -} - -func (c *ConsulLock) Unlock() error { - c.mu.Lock() - defer c.mu.Unlock() - - if c.session != nil { - c.session.cleanup() - c.session = nil - // Don't clear c.sessionID since we rely on returning the same old ID after - // Unlock until the next Lock. - } - return nil -} - -func (c *ConsulLock) Value() (bool, string, error) { - kv := c.client.KV() - - var queryOptions *api.QueryOptions - if c.consistencyMode == consistencyModeStrong { - queryOptions = &api.QueryOptions{ - RequireConsistent: true, - } - } - - pair, _, err := kv.Get(c.key, queryOptions) - if err != nil { - return false, "", err - } - if pair == nil { - return false, "", nil - } - // Note that held is expected to mean "does _any_ node hold the lock" not - // "does this current instance hold the lock" so although we know what our own - // session ID is, we don't check it matches here only that there is _some_ - // session in Consul holding the lock right now. - held := pair.Session != "" - value := string(pair.Value) - return held, value, nil -} - -func (c *ConsulLock) Info() (key, sessionid string) { - c.mu.Lock() - defer c.mu.Unlock() - - return c.key, c.sessionID -} diff --git a/physical/consul/consul_test.go b/physical/consul/consul_test.go deleted file mode 100644 index b01ddd210..000000000 --- a/physical/consul/consul_test.go +++ /dev/null @@ -1,528 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package consul - -import ( - "bytes" - "context" - "encoding/hex" - "fmt" - "math/rand" - "reflect" - "strings" - "testing" - "time" - - "github.com/hashicorp/consul/api" - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/helper/testhelpers/consul" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/physical" - "github.com/stretchr/testify/require" -) - -func TestConsul_newConsulBackend(t *testing.T) { - tests := []struct { - name string - consulConfig map[string]string - fail bool - redirectAddr string - checkTimeout time.Duration - path string - service string - address string - scheme string - token string - max_parallel int - disableReg bool - consistencyMode string - }{ - { - name: "Valid default config", - consulConfig: map[string]string{}, - checkTimeout: 5 * time.Second, - redirectAddr: "http://127.0.0.1:8200", - path: "vault/", - service: "vault", - address: "127.0.0.1:8500", - scheme: "http", - token: "", - max_parallel: 4, - disableReg: false, - consistencyMode: "default", - }, - { - name: "Valid modified config", - consulConfig: map[string]string{ - "path": "seaTech/", - "service": "astronomy", - "redirect_addr": "http://127.0.0.2:8200", - "check_timeout": "6s", - "address": "127.0.0.2", - "scheme": "https", - "token": "deadbeef-cafeefac-deadc0de-feedface", - "max_parallel": "4", - "disable_registration": "false", - "consistency_mode": "strong", - }, - checkTimeout: 6 * time.Second, - path: "seaTech/", - service: "astronomy", - redirectAddr: "http://127.0.0.2:8200", - address: "127.0.0.2", - scheme: "https", - token: "deadbeef-cafeefac-deadc0de-feedface", - max_parallel: 4, - consistencyMode: "strong", - }, - { - name: "Unix socket", - consulConfig: map[string]string{ - "address": "unix:///tmp/.consul.http.sock", - }, - address: "/tmp/.consul.http.sock", - scheme: "http", // Default, not overridden? - - // Defaults - checkTimeout: 5 * time.Second, - redirectAddr: "http://127.0.0.1:8200", - path: "vault/", - service: "vault", - token: "", - max_parallel: 4, - disableReg: false, - consistencyMode: "default", - }, - { - name: "Scheme in address", - consulConfig: map[string]string{ - "address": "https://127.0.0.2:5000", - }, - address: "127.0.0.2:5000", - scheme: "https", - - // Defaults - checkTimeout: 5 * time.Second, - redirectAddr: "http://127.0.0.1:8200", - path: "vault/", - service: "vault", - token: "", - max_parallel: 4, - disableReg: false, - consistencyMode: "default", - }, - } - - for _, test := range tests { - logger := logging.NewVaultLogger(log.Debug) - - be, err := NewConsulBackend(test.consulConfig, logger) - if test.fail { - if err == nil { - t.Fatalf(`Expected config "%s" to fail`, test.name) - } else { - continue - } - } else if !test.fail && err != nil { - t.Fatalf("Expected config %s to not fail: %v", test.name, err) - } - - c, ok := be.(*ConsulBackend) - if !ok { - t.Fatalf("Expected ConsulBackend: %s", test.name) - } - - if test.path != c.path { - t.Errorf("bad: %s %v != %v", test.name, test.path, c.path) - } - - if test.consistencyMode != c.consistencyMode { - t.Errorf("bad consistency_mode value: %v != %v", test.consistencyMode, c.consistencyMode) - } - - // The configuration stored in the Consul "client" object is not exported, so - // we either have to skip validating it, or add a method to export it, or use reflection. - consulConfig := reflect.Indirect(reflect.ValueOf(c.client)).FieldByName("config") - consulConfigScheme := consulConfig.FieldByName("Scheme").String() - consulConfigAddress := consulConfig.FieldByName("Address").String() - - if test.scheme != consulConfigScheme { - t.Errorf("bad scheme value: %v != %v", test.scheme, consulConfigScheme) - } - - if test.address != consulConfigAddress { - t.Errorf("bad address value: %v != %v", test.address, consulConfigAddress) - } - - // FIXME(sean@): Unable to test max_parallel - // if test.max_parallel != cap(c.permitPool) { - // t.Errorf("bad: %v != %v", test.max_parallel, cap(c.permitPool)) - // } - } -} - -func TestConsulBackend(t *testing.T) { - cleanup, config := consul.PrepareTestContainer(t, "1.4.4", false, true) - defer cleanup() - - client, err := api.NewClient(config.APIConfig()) - if err != nil { - t.Fatalf("err: %v", err) - } - - randPath := fmt.Sprintf("vault-%d/", time.Now().Unix()) - defer func() { - client.KV().DeleteTree(randPath, nil) - }() - - logger := logging.NewVaultLogger(log.Debug) - - b, err := NewConsulBackend(map[string]string{ - "address": config.Address(), - "token": config.Token, - "path": randPath, - "max_parallel": "256", - }, logger) - if err != nil { - t.Fatalf("err: %s", err) - } - - physical.ExerciseBackend(t, b) - physical.ExerciseBackend_ListPrefix(t, b) -} - -func TestConsul_TooLarge(t *testing.T) { - cleanup, config := consul.PrepareTestContainer(t, "1.4.4", false, true) - defer cleanup() - - client, err := api.NewClient(config.APIConfig()) - if err != nil { - t.Fatalf("err: %v", err) - } - - randPath := fmt.Sprintf("vault-%d/", time.Now().Unix()) - defer func() { - client.KV().DeleteTree(randPath, nil) - }() - - logger := logging.NewVaultLogger(log.Debug) - - b, err := NewConsulBackend(map[string]string{ - "address": config.Address(), - "token": config.Token, - "path": randPath, - "max_parallel": "256", - }, logger) - if err != nil { - t.Fatalf("err: %s", err) - } - - zeros := make([]byte, 600000) - n, err := rand.Read(zeros) - if n != 600000 { - t.Fatalf("expected 500k zeros, read %d", n) - } - if err != nil { - t.Fatal(err) - } - - err = b.Put(context.Background(), &physical.Entry{ - Key: "foo", - Value: zeros, - }) - if err == nil { - t.Fatal("expected error") - } - if !strings.Contains(err.Error(), physical.ErrValueTooLarge) { - t.Fatalf("expected value too large error, got %v", err) - } - - err = b.(physical.Transactional).Transaction(context.Background(), []*physical.TxnEntry{ - { - Operation: physical.PutOperation, - Entry: &physical.Entry{ - Key: "foo", - Value: zeros, - }, - }, - }) - if err == nil { - t.Fatal("expected error") - } - if !strings.Contains(err.Error(), physical.ErrValueTooLarge) { - t.Fatalf("expected value too large error, got %v", err) - } -} - -func TestConsul_ExpandedCapabilitiesAvailable(t *testing.T) { - testCases := map[string]bool{ - "1.13.5": false, - "1.14.3": true, - } - - for version, shouldBeAvailable := range testCases { - t.Run(version, func(t *testing.T) { - cleanup, config := consul.PrepareTestContainer(t, version, false, true) - defer cleanup() - - logger := logging.NewVaultLogger(log.Debug) - backendConfig := map[string]string{ - "address": config.Address(), - "token": config.Token, - "path": "vault/", - "max_parallel": "-1", - } - - be, err := NewConsulBackend(backendConfig, logger) - if err != nil { - t.Fatal(err) - } - b := be.(*ConsulBackend) - - isAvailable := b.ExpandedCapabilitiesAvailable(context.Background()) - if isAvailable != shouldBeAvailable { - t.Errorf("%t != %t, version %s\n", isAvailable, shouldBeAvailable, version) - } - }) - } -} - -func TestConsul_TransactionalBackend_GetTransactionsForNonExistentValues(t *testing.T) { - cleanup, config := consul.PrepareTestContainer(t, "1.14.2", false, true) - defer cleanup() - - client, err := api.NewClient(config.APIConfig()) - if err != nil { - t.Fatal(err) - } - - txns := make([]*physical.TxnEntry, 0) - ctx := context.Background() - logger := logging.NewVaultLogger(log.Debug) - backendConfig := map[string]string{ - "address": config.Address(), - "token": config.Token, - "path": "vault/", - "max_parallel": "-1", - } - - be, err := NewConsulBackend(backendConfig, logger) - if err != nil { - t.Fatal(err) - } - b := be.(*ConsulBackend) - - defer func() { - _, _ = client.KV().DeleteTree("foo/", nil) - }() - - txns = append(txns, &physical.TxnEntry{ - Operation: physical.GetOperation, - Entry: &physical.Entry{ - Key: "foo/bar", - }, - }) - txns = append(txns, &physical.TxnEntry{ - Operation: physical.PutOperation, - Entry: &physical.Entry{ - Key: "foo/bar", - Value: []byte("baz"), - }, - }) - - err = b.Transaction(ctx, txns) - if err != nil { - t.Fatal(err) - } - - // This should return nil, because the key foo/bar didn't exist when we ran that transaction, so the get - // should return nil, and the put always returns nil - for _, txn := range txns { - if txn.Operation == physical.GetOperation { - if txn.Entry.Value != nil { - t.Fatalf("expected txn.entry.value to be nil but it was %q", string(txn.Entry.Value)) - } - } - } -} - -// TestConsul_TransactionalBackend_GetTransactions tests that passing a slice of transactions to the -// consul backend will populate values for any transactions that are Get operations. -func TestConsul_TransactionalBackend_GetTransactions(t *testing.T) { - cleanup, config := consul.PrepareTestContainer(t, "1.14.2", false, true) - defer cleanup() - - client, err := api.NewClient(config.APIConfig()) - if err != nil { - t.Fatal(err) - } - - txns := make([]*physical.TxnEntry, 0) - ctx := context.Background() - logger := logging.NewVaultLogger(log.Debug) - backendConfig := map[string]string{ - "address": config.Address(), - "token": config.Token, - "path": "vault/", - "max_parallel": "-1", - } - - be, err := NewConsulBackend(backendConfig, logger) - if err != nil { - t.Fatal(err) - } - b := be.(*ConsulBackend) - - defer func() { - _, _ = client.KV().DeleteTree("foo/", nil) - }() - - // Add some seed values to consul, and prepare our slice of transactions at the same time - for i := 0; i < 64; i++ { - key := fmt.Sprintf("foo/lol-%d", i) - err := b.Put(ctx, &physical.Entry{Key: key, Value: []byte(fmt.Sprintf("value-%d", i))}) - if err != nil { - t.Fatal(err) - } - - txns = append(txns, &physical.TxnEntry{ - Operation: physical.GetOperation, - Entry: &physical.Entry{ - Key: key, - }, - }) - } - - for i := 0; i < 64; i++ { - key := fmt.Sprintf("foo/lol-%d", i) - if i%2 == 0 { - txns = append(txns, &physical.TxnEntry{ - Operation: physical.PutOperation, - Entry: &physical.Entry{ - Key: key, - Value: []byte("lmao"), - }, - }) - } else { - txns = append(txns, &physical.TxnEntry{ - Operation: physical.DeleteOperation, - Entry: &physical.Entry{ - Key: key, - }, - }) - } - } - - if len(txns) != 128 { - t.Fatal("wrong number of transactions") - } - - err = b.Transaction(ctx, txns) - if err != nil { - t.Fatal(err) - } - - // Check that our Get operations were populated with their values - for i, txn := range txns { - if txn.Operation == physical.GetOperation { - val := []byte(fmt.Sprintf("value-%d", i)) - if !bytes.Equal(val, txn.Entry.Value) { - t.Fatalf("expected %s to equal %s but it didn't", hex.EncodeToString(val), hex.EncodeToString(txn.Entry.Value)) - } - } - } -} - -func TestConsulHABackend(t *testing.T) { - cleanup, config := consul.PrepareTestContainer(t, "1.4.4", false, true) - defer cleanup() - - client, err := api.NewClient(config.APIConfig()) - if err != nil { - t.Fatalf("err: %v", err) - } - - // We used to use a timestamp here but then if you run multiple instances in - // parallel with one Consul they end up conflicting. - randPath := fmt.Sprintf("vault-%d/", rand.Int()) - defer func() { - client.KV().DeleteTree(randPath, nil) - }() - - logger := logging.NewVaultLogger(log.Debug) - backendConfig := map[string]string{ - "address": config.Address(), - "token": config.Token, - "path": randPath, - "max_parallel": "-1", - // We have to wait this out as part of the test so shorten it a little from - // the default 15 seconds helps with test run times, especially when running - // this in a loop to detect flakes! - "lock_wait_time": "3s", - } - - b, err := NewConsulBackend(backendConfig, logger) - if err != nil { - t.Fatalf("err: %s", err) - } - - b2, err := NewConsulBackend(backendConfig, logger) - if err != nil { - t.Fatalf("err: %s", err) - } - - physical.ExerciseHABackend(t, b.(physical.HABackend), b2.(physical.HABackend)) - - detect, ok := b.(physical.RedirectDetect) - if !ok { - t.Fatalf("consul does not implement RedirectDetect") - } - host, err := detect.DetectHostAddr() - if err != nil { - t.Fatalf("err: %s", err) - } - if host == "" { - t.Fatalf("bad addr: %v", host) - } - - // Calling `Info` on a Lock that has been unlocked must still return the old - // sessionID (until it is locked again) otherwise we will fail to fence writes - // that are still in flight from before (e.g. queued WAL or Merkle flushes) as - // soon as the first one unlocks the session allowing corruption again. - l, err := b.(physical.HABackend).LockWith("test-lock-session-info", "bar") - require.NoError(t, err) - - expectKey := randPath + "test-lock-session-info" - - cl := l.(*ConsulLock) - - stopCh := make(chan struct{}) - time.AfterFunc(5*time.Second, func() { - close(stopCh) - }) - leaderCh, err := cl.Lock(stopCh) - require.NoError(t, err) - require.NotNil(t, leaderCh) - - key, sid := cl.Info() - require.Equal(t, expectKey, key) - require.NotEmpty(t, sid) - - // Now Unlock the lock, sessionID should be reset to empty string - err = cl.Unlock() - require.NoError(t, err) - key2, sid2 := cl.Info() - require.Equal(t, key, key2) - require.Equal(t, sid, sid2) - - // Lock it again, this should cause a new session to be created so SID should - // change. - leaderCh, err = cl.Lock(stopCh) - require.NoError(t, err) - require.NotNil(t, leaderCh) - - key3, sid3 := cl.Info() - require.Equal(t, key, key3) - require.NotEqual(t, sid, sid3) -} diff --git a/physical/consul/helpers.go b/physical/consul/helpers.go deleted file mode 100644 index 2f6ac574b..000000000 --- a/physical/consul/helpers.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package consul - -import ( - "math/rand" - "time" -) - -// DurationMinusBuffer returns a duration, minus a buffer and jitter -// subtracted from the duration. This function is used primarily for -// servicing Consul TTL Checks in advance of the TTL. -func DurationMinusBuffer(intv time.Duration, buffer time.Duration, jitter int64) time.Duration { - d := intv - buffer - if jitter == 0 { - d -= RandomStagger(d) - } else { - d -= RandomStagger(time.Duration(int64(d) / jitter)) - } - return d -} - -// DurationMinusBufferDomain returns the domain of valid durations from a -// call to DurationMinusBuffer. This function is used to check user -// specified input values to DurationMinusBuffer. -func DurationMinusBufferDomain(intv time.Duration, buffer time.Duration, jitter int64) (min time.Duration, max time.Duration) { - max = intv - buffer - if jitter == 0 { - min = max - } else { - min = max - time.Duration(int64(max)/jitter) - } - return min, max -} - -// RandomStagger returns an interval between 0 and the duration -func RandomStagger(intv time.Duration) time.Duration { - if intv == 0 { - return 0 - } - return time.Duration(uint64(rand.Int63()) % uint64(intv)) -} diff --git a/scripts/gen_openapi.sh b/scripts/gen_openapi.sh index afe93ed64..798c70f48 100755 --- a/scripts/gen_openapi.sh +++ b/scripts/gen_openapi.sh @@ -64,7 +64,6 @@ vault auth enable "radius" vault auth enable "userpass" # Enable secrets plugins -vault secrets enable "consul" vault secrets enable "database" vault secrets enable "kubernetes" vault secrets enable -path="kv-v1/" -version=1 "kv" diff --git a/ui/app/helpers/mountable-secret-engines.js b/ui/app/helpers/mountable-secret-engines.js index f9a95e167..b1c5ad34a 100644 --- a/ui/app/helpers/mountable-secret-engines.js +++ b/ui/app/helpers/mountable-secret-engines.js @@ -35,11 +35,6 @@ const MOUNTABLE_SECRET_ENGINES = [ type: 'ad', category: 'cloud', }, - { - displayName: 'Consul', - type: 'consul', - category: 'infra', - }, { displayName: 'Databases', type: 'database', diff --git a/ui/app/templates/components/wizard/consul-engine.hbs b/ui/app/templates/components/wizard/consul-engine.hbs deleted file mode 100644 index e3efa845a..000000000 --- a/ui/app/templates/components/wizard/consul-engine.hbs +++ /dev/null @@ -1,15 +0,0 @@ -{{! - Copyright (c) HashiCorp, Inc. - SPDX-License-Identifier: BUSL-1.1 -~}} - - -

- The Consul Secrets Engine generates Consul API tokens dynamically based on Consul ACL policies. -

-
\ No newline at end of file diff --git a/ui/public/eco/consul.svg b/ui/public/eco/consul.svg deleted file mode 100644 index fda270c18..000000000 --- a/ui/public/eco/consul.svg +++ /dev/null @@ -1 +0,0 @@ -