diff --git a/command/commands.go b/command/commands.go index a9b31fccf..874322e77 100644 --- a/command/commands.go +++ b/command/commands.go @@ -45,7 +45,6 @@ import ( physCassandra "github.com/hashicorp/vault/physical/cassandra" physCockroachDB "github.com/hashicorp/vault/physical/cockroachdb" physConsul "github.com/hashicorp/vault/physical/consul" - physCouchDB "github.com/hashicorp/vault/physical/couchdb" physFoundationDB "github.com/hashicorp/vault/physical/foundationdb" physMySQL "github.com/hashicorp/vault/physical/mysql" physOCI "github.com/hashicorp/vault/physical/oci" @@ -175,8 +174,6 @@ var ( "cassandra": physCassandra.NewCassandraBackend, "cockroachdb": physCockroachDB.NewCockroachDBBackend, "consul": physConsul.NewConsulBackend, - "couchdb_transactional": physCouchDB.NewTransactionalCouchDBBackend, - "couchdb": physCouchDB.NewCouchDBBackend, "file_transactional": physFile.NewTransactionalFileBackend, "file": physFile.NewFileBackend, "foundationdb": physFoundationDB.NewFDBBackend, diff --git a/physical/couchdb/couchdb.go b/physical/couchdb/couchdb.go deleted file mode 100644 index 0a1c379c6..000000000 --- a/physical/couchdb/couchdb.go +++ /dev/null @@ -1,317 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package couchdb - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "os" - "strconv" - "strings" - "time" - - metrics "github.com/armon/go-metrics" - cleanhttp "github.com/hashicorp/go-cleanhttp" - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/sdk/physical" -) - -// CouchDBBackend allows the management of couchdb users -type CouchDBBackend struct { - logger log.Logger - client *couchDBClient - permitPool *physical.PermitPool -} - -// Verify CouchDBBackend satisfies the correct interfaces -var ( - _ physical.Backend = (*CouchDBBackend)(nil) - _ physical.PseudoTransactional = (*CouchDBBackend)(nil) - _ physical.PseudoTransactional = (*TransactionalCouchDBBackend)(nil) -) - -type couchDBClient struct { - endpoint string - username string - password string - *http.Client -} - -type couchDBListItem struct { - ID string `json:"id"` - Key string `json:"key"` - Value struct { - Revision string - } `json:"value"` -} - -type couchDBList struct { - TotalRows int `json:"total_rows"` - Offset int `json:"offset"` - Rows []couchDBListItem `json:"rows"` -} - -func (m *couchDBClient) rev(key string) (string, error) { - req, err := http.NewRequest("HEAD", fmt.Sprintf("%s/%s", m.endpoint, key), nil) - if err != nil { - return "", err - } - req.SetBasicAuth(m.username, m.password) - - resp, err := m.Client.Do(req) - if err != nil { - return "", err - } - resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return "", nil - } - etag := resp.Header.Get("Etag") - if len(etag) < 2 { - return "", nil - } - return etag[1 : len(etag)-1], nil -} - -func (m *couchDBClient) put(e couchDBEntry) error { - bs, err := json.Marshal(e) - if err != nil { - return err - } - - req, err := http.NewRequest("PUT", fmt.Sprintf("%s/%s", m.endpoint, e.ID), bytes.NewReader(bs)) - if err != nil { - return err - } - req.SetBasicAuth(m.username, m.password) - resp, err := m.Client.Do(req) - if err == nil { - resp.Body.Close() - } - - return err -} - -func (m *couchDBClient) get(key string) (*physical.Entry, error) { - req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s", m.endpoint, url.PathEscape(key)), nil) - if err != nil { - return nil, err - } - req.SetBasicAuth(m.username, m.password) - resp, err := m.Client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode == http.StatusNotFound { - return nil, nil - } else if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("GET returned %q", resp.Status) - } - bs, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - entry := couchDBEntry{} - if err := json.Unmarshal(bs, &entry); err != nil { - return nil, err - } - return entry.Entry, nil -} - -func (m *couchDBClient) list(prefix string) ([]couchDBListItem, error) { - req, _ := http.NewRequest("GET", fmt.Sprintf("%s/_all_docs", m.endpoint), nil) - req.SetBasicAuth(m.username, m.password) - values := req.URL.Query() - values.Set("skip", "0") - values.Set("include_docs", "false") - if prefix != "" { - values.Set("startkey", fmt.Sprintf("%q", prefix)) - values.Set("endkey", fmt.Sprintf("%q", prefix+"{}")) - } - req.URL.RawQuery = values.Encode() - - resp, err := m.Client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - results := couchDBList{} - if err := json.Unmarshal(data, &results); err != nil { - return nil, err - } - - return results.Rows, nil -} - -func buildCouchDBBackend(conf map[string]string, logger log.Logger) (*CouchDBBackend, error) { - endpoint := os.Getenv("COUCHDB_ENDPOINT") - if endpoint == "" { - endpoint = conf["endpoint"] - } - if endpoint == "" { - return nil, fmt.Errorf("missing endpoint") - } - - username := os.Getenv("COUCHDB_USERNAME") - if username == "" { - username = conf["username"] - } - - password := os.Getenv("COUCHDB_PASSWORD") - if password == "" { - password = conf["password"] - } - - maxParStr, ok := conf["max_parallel"] - var maxParInt int - var err error - 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) - } - } - - return &CouchDBBackend{ - client: &couchDBClient{ - endpoint: endpoint, - username: username, - password: password, - Client: cleanhttp.DefaultPooledClient(), - }, - logger: logger, - permitPool: physical.NewPermitPool(maxParInt), - }, nil -} - -func NewCouchDBBackend(conf map[string]string, logger log.Logger) (physical.Backend, error) { - return buildCouchDBBackend(conf, logger) -} - -type couchDBEntry struct { - Entry *physical.Entry `json:"entry"` - Rev string `json:"_rev,omitempty"` - ID string `json:"_id"` - Deleted *bool `json:"_deleted,omitempty"` -} - -// Put is used to insert or update an entry -func (m *CouchDBBackend) Put(ctx context.Context, entry *physical.Entry) error { - m.permitPool.Acquire() - defer m.permitPool.Release() - - return m.PutInternal(ctx, entry) -} - -// Get is used to fetch an entry -func (m *CouchDBBackend) Get(ctx context.Context, key string) (*physical.Entry, error) { - m.permitPool.Acquire() - defer m.permitPool.Release() - - return m.GetInternal(ctx, key) -} - -// Delete is used to permanently delete an entry -func (m *CouchDBBackend) Delete(ctx context.Context, key string) error { - m.permitPool.Acquire() - defer m.permitPool.Release() - - return m.DeleteInternal(ctx, key) -} - -// List is used to list all the keys under a given prefix -func (m *CouchDBBackend) List(ctx context.Context, prefix string) ([]string, error) { - defer metrics.MeasureSince([]string{"couchdb", "list"}, time.Now()) - - m.permitPool.Acquire() - defer m.permitPool.Release() - - items, err := m.client.list(prefix) - if err != nil { - return nil, err - } - - var out []string - seen := make(map[string]interface{}) - for _, result := range items { - trimmed := strings.TrimPrefix(result.ID, prefix) - sep := strings.Index(trimmed, "/") - if sep == -1 { - out = append(out, trimmed) - } else { - trimmed = trimmed[:sep+1] - if _, ok := seen[trimmed]; !ok { - out = append(out, trimmed) - seen[trimmed] = struct{}{} - } - } - } - return out, nil -} - -// TransactionalCouchDBBackend creates a couchdb backend that forces all operations to happen -// in serial -type TransactionalCouchDBBackend struct { - CouchDBBackend -} - -func NewTransactionalCouchDBBackend(conf map[string]string, logger log.Logger) (physical.Backend, error) { - backend, err := buildCouchDBBackend(conf, logger) - if err != nil { - return nil, err - } - backend.permitPool = physical.NewPermitPool(1) - - return &TransactionalCouchDBBackend{ - CouchDBBackend: *backend, - }, nil -} - -// GetInternal is used to fetch an entry -func (m *CouchDBBackend) GetInternal(ctx context.Context, key string) (*physical.Entry, error) { - defer metrics.MeasureSince([]string{"couchdb", "get"}, time.Now()) - - return m.client.get(key) -} - -// PutInternal is used to insert or update an entry -func (m *CouchDBBackend) PutInternal(ctx context.Context, entry *physical.Entry) error { - defer metrics.MeasureSince([]string{"couchdb", "put"}, time.Now()) - - revision, _ := m.client.rev(url.PathEscape(entry.Key)) - - return m.client.put(couchDBEntry{ - Entry: entry, - Rev: revision, - ID: url.PathEscape(entry.Key), - }) -} - -// DeleteInternal is used to permanently delete an entry -func (m *CouchDBBackend) DeleteInternal(ctx context.Context, key string) error { - defer metrics.MeasureSince([]string{"couchdb", "delete"}, time.Now()) - - revision, _ := m.client.rev(url.PathEscape(key)) - deleted := true - return m.client.put(couchDBEntry{ - ID: url.PathEscape(key), - Rev: revision, - Deleted: &deleted, - }) -} diff --git a/physical/couchdb/couchdb_test.go b/physical/couchdb/couchdb_test.go deleted file mode 100644 index 724e13485..000000000 --- a/physical/couchdb/couchdb_test.go +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package couchdb - -import ( - "context" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "os" - "strings" - "testing" - "time" - - log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/sdk/helper/docker" - "github.com/hashicorp/vault/sdk/helper/logging" - "github.com/hashicorp/vault/sdk/physical" -) - -func TestCouchDBBackend(t *testing.T) { - cleanup, config := prepareCouchdbDBTestContainer(t) - defer cleanup() - - logger := logging.NewVaultLogger(log.Debug) - - b, err := NewCouchDBBackend(map[string]string{ - "endpoint": config.URL().String(), - "username": config.username, - "password": config.password, - }, logger) - if err != nil { - t.Fatalf("err: %s", err) - } - - physical.ExerciseBackend(t, b) - physical.ExerciseBackend_ListPrefix(t, b) -} - -func TestTransactionalCouchDBBackend(t *testing.T) { - cleanup, config := prepareCouchdbDBTestContainer(t) - defer cleanup() - - logger := logging.NewVaultLogger(log.Debug) - - b, err := NewTransactionalCouchDBBackend(map[string]string{ - "endpoint": config.URL().String(), - "username": config.username, - "password": config.password, - }, logger) - if err != nil { - t.Fatalf("err: %s", err) - } - - physical.ExerciseBackend(t, b) - physical.ExerciseBackend_ListPrefix(t, b) -} - -type couchDB struct { - baseURL url.URL - dbname string - username string - password string -} - -func (c couchDB) Address() string { - return c.baseURL.Host -} - -func (c couchDB) URL() *url.URL { - u := c.baseURL - u.Path = c.dbname - return &u -} - -var _ docker.ServiceConfig = &couchDB{} - -func prepareCouchdbDBTestContainer(t *testing.T) (func(), *couchDB) { - // If environment variable is set, assume caller wants to target a real - // DynamoDB. - if os.Getenv("COUCHDB_ENDPOINT") != "" { - return func() {}, &couchDB{ - baseURL: url.URL{Host: os.Getenv("COUCHDB_ENDPOINT")}, - username: os.Getenv("COUCHDB_USERNAME"), - password: os.Getenv("COUCHDB_PASSWORD"), - } - } - - runner, err := docker.NewServiceRunner(docker.RunOptions{ - ContainerName: "couchdb", - ImageRepo: "docker.mirror.hashicorp.services/library/couchdb", - ImageTag: "1.6", - Ports: []string{"5984/tcp"}, - DoNotAutoRemove: true, - }) - if err != nil { - t.Fatalf("Could not start local CouchDB: %s", err) - } - - svc, err := runner.StartService(context.Background(), setupCouchDB) - if err != nil { - t.Fatalf("Could not start local CouchDB: %s", err) - } - - return svc.Cleanup, svc.Config.(*couchDB) -} - -func setupCouchDB(ctx context.Context, host string, port int) (docker.ServiceConfig, error) { - c := &couchDB{ - baseURL: url.URL{Scheme: "http", Host: fmt.Sprintf("%s:%d", host, port)}, - dbname: fmt.Sprintf("vault-test-%d", time.Now().Unix()), - username: "admin", - password: "admin", - } - - { - resp, err := http.Get(c.baseURL.String()) - if err != nil { - return nil, err - } - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("expected couchdb to return status code 200, got (%s) instead", resp.Status) - } - } - - { - req, err := http.NewRequest("PUT", c.URL().String(), nil) - if err != nil { - return nil, fmt.Errorf("could not create create database request: %q", err) - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, fmt.Errorf("could not create database: %q", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusCreated { - bs, _ := ioutil.ReadAll(resp.Body) - return nil, fmt.Errorf("failed to create database: %s %s\n", resp.Status, string(bs)) - } - } - - { - u := c.baseURL - u.Path = fmt.Sprintf("_config/admins/%s", c.username) - req, err := http.NewRequest("PUT", u.String(), strings.NewReader(fmt.Sprintf(`"%s"`, c.password))) - if err != nil { - return nil, fmt.Errorf("Could not create admin user request: %q", err) - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, fmt.Errorf("Could not create admin user: %q", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - bs, _ := ioutil.ReadAll(resp.Body) - return nil, fmt.Errorf("Failed to create admin user: %s %s\n", resp.Status, string(bs)) - } - } - - return c, nil -}