open-vault/vendor/github.com/hashicorp/vault-plugin-database-elasticsearch/elasticsearch.go
2019-06-20 15:56:24 -04:00

354 lines
12 KiB
Go

package elasticsearch
import (
"context"
"encoding/json"
"errors"
"fmt"
"sync"
"time"
"github.com/hashicorp/errwrap"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/sdk/database/dbplugin"
"github.com/hashicorp/vault/sdk/database/helper/credsutil"
"github.com/hashicorp/vault/sdk/database/helper/dbutil"
)
func New() (interface{}, error) {
db := NewElasticsearch()
return dbplugin.NewDatabaseErrorSanitizerMiddleware(db, db.SecretValues), nil
}
func Run(apiTLSConfig *api.TLSConfig) error {
dbplugin.Serve(NewElasticsearch(), api.VaultPluginTLSProvider(apiTLSConfig))
return nil
}
func NewElasticsearch() *Elasticsearch {
return &Elasticsearch{
credentialProducer: &credsutil.SQLCredentialsProducer{
DisplayNameLen: 15,
RoleNameLen: 15,
UsernameLen: 100,
Separator: "-",
},
}
}
// Elasticsearch implements dbplugin's Database interface.
type Elasticsearch struct {
// The CredentialsProducer is never mutated and thus is inherently thread-safe.
credentialProducer credsutil.CredentialsProducer
// This protects the config from races while also allowing multiple threads
// to read the config simultaneously when it's not changing.
mux sync.RWMutex
// The root credential config.
config map[string]interface{}
}
func (es *Elasticsearch) Type() (string, error) {
return "elasticsearch", nil
}
// SecretValues is used by some error-sanitizing middleware in Vault that basically
// replaces the keys in the map with the values given so they're not leaked via
// error messages.
func (es *Elasticsearch) SecretValues() map[string]interface{} {
es.mux.RLock()
defer es.mux.RUnlock()
replacements := make(map[string]interface{})
for _, secretKey := range []string{"password", "client_key"} {
vIfc, found := es.config[secretKey]
if !found {
continue
}
secretVal, ok := vIfc.(string)
if !ok {
continue
}
// So, supposing a password of "0pen5e5ame",
// this will cause that string to get replaced with "[password]".
replacements[secretVal] = "[" + secretKey + "]"
}
return replacements
}
// Init is called on `$ vault write database/config/:db-name`,
// or when you do a creds call after Vault's been restarted.
func (es *Elasticsearch) Init(ctx context.Context, config map[string]interface{}, verifyConnection bool) (map[string]interface{}, error) {
// Validate the config to provide immediate feedback to the user.
// Ensure required string fields are provided in the expected format.
for _, requiredField := range []string{"username", "password", "url"} {
raw, ok := config[requiredField]
if !ok {
return nil, fmt.Errorf(`%q must be provided`, requiredField)
}
if _, ok := raw.(string); !ok {
return nil, fmt.Errorf(`%q must be a string`, requiredField)
}
}
// Ensure optional string fields are provided in the expected format.
for _, optionalField := range []string{"ca_cert", "ca_path", "client_cert", "client_key", "tls_server_name"} {
raw, ok := config[optionalField]
if !ok {
continue
}
if _, ok = raw.(string); !ok {
return nil, fmt.Errorf(`%q must be a string`, optionalField)
}
}
// Check the one optional bool field is in the expected format.
if raw, ok := config["insecure"]; ok {
if _, ok = raw.(bool); !ok {
return nil, errors.New(`"insecure" must be a bool`)
}
}
// Test the given config to see if we can make a client.
client, err := buildClient(config)
if err != nil {
return nil, errwrap.Wrapf("couldn't make client with inbound config: {{err}}", err)
}
// Optionally, test the given config to see if we can make a successful call.
if verifyConnection {
// Whether this role is found or unfound, if we're configured correctly there will
// be no err from the call. However, if something is misconfigured, this will yield
// an error response, which will be described in the returned error.
if _, err := client.GetRole(ctx, "vault-test"); err != nil {
return nil, errwrap.Wrapf("client test of getting a role failed: {{err}}", err)
}
}
// Everything's working, write the new config to memory and storage.
es.mux.Lock()
defer es.mux.Unlock()
es.config = config
return es.config, nil
}
// CreateUser is called on `$ vault read database/creds/:role-name`
// and it's the first time anything is touched from `$ vault write database/roles/:role-name`.
// This is likely to be the highest-throughput method for this plugin.
func (es *Elasticsearch) CreateUser(ctx context.Context, statements dbplugin.Statements, usernameConfig dbplugin.UsernameConfig, _ time.Time) (string, string, error) {
username, err := es.credentialProducer.GenerateUsername(usernameConfig)
if err != nil {
return "", "", errwrap.Wrapf(fmt.Sprintf("unable to generate username for %q: {{err}}", usernameConfig), err)
}
password, err := es.credentialProducer.GeneratePassword()
if err != nil {
return "", "", errwrap.Wrapf("unable to generate password: {{err}}", err)
}
stmt, err := newCreationStatement(statements)
if err != nil {
return "", "", errwrap.Wrapf("unable to read creation_statements: {{err}}", err)
}
user := &User{
Password: password,
Roles: stmt.PreexistingRoles,
}
// Don't let anyone write the config while we're using it for our current client.
es.mux.RLock()
defer es.mux.RUnlock()
client, err := buildClient(es.config)
if err != nil {
return "", "", errwrap.Wrapf("unable to get client: {{err}}", err)
}
// If the RoleToCreate map has been populated with any data, we have one role to create.
// There can either be one RoleToCreate and no PreexistingRoles, or >= 1 PreexistingRoles
// and no RoleToCreate. They're mutually exclusive.
if len(stmt.RoleToCreate) > 0 {
// We'll simply name the role the same thing as the username, making it easy to tie back to this user.
if err := client.CreateRole(ctx, username, stmt.RoleToCreate); err != nil {
return "", "", errwrap.Wrapf(fmt.Sprintf("unable to create role name %s, role definition %q: {{err}}", username, stmt.RoleToCreate), err)
}
user.Roles = []string{username}
}
if err := client.CreateUser(ctx, username, user); err != nil {
return "", "", errwrap.Wrapf(fmt.Sprintf("unable to create user name %s, user %q: {{err}}", username, user), err)
}
return username, password, nil
}
// RenewUser gets called on `$ vault lease renew {{lease-id}}`. It automatically pushes out the amount of time until
// the database secrets engine calls RevokeUser, if appropriate.
func (es *Elasticsearch) RenewUser(_ context.Context, _ dbplugin.Statements, _ string, _ time.Time) error {
// Normally, this function would update a "VALID UNTIL" statement on a database user
// but there's no similar need here.
return nil
}
// RevokeUser is called when a lease expires.
func (es *Elasticsearch) RevokeUser(ctx context.Context, statements dbplugin.Statements, username string) error {
stmt, err := newCreationStatement(statements)
if err != nil {
return errwrap.Wrapf("unable to read creation_statements: {{err}}", err)
}
// Don't let anyone write the config while we're using it for our current client.
es.mux.RLock()
defer es.mux.RUnlock()
client, err := buildClient(es.config)
if err != nil {
return errwrap.Wrapf("unable to get client: {{err}}", err)
}
var errs error
if len(stmt.RoleToCreate) > 0 {
// If the role already doesn't exist because it was successfully deleted on a previous
// attempt to run this code, there will be no error, so it's harmless to try.
if err := client.DeleteRole(ctx, username); err != nil {
errs = multierror.Append(errs, errwrap.Wrapf(fmt.Sprintf("unable to delete role name %s: {{err}}", username), err))
}
}
// Same with the user. If it was already deleted on a previous attempt, there won't be an
// error.
if err := client.DeleteUser(ctx, username); err != nil {
errs = multierror.Append(errs, errwrap.Wrapf(fmt.Sprintf("unable to create user name %s: {{err}}", username), err))
}
return errs
}
// RotateRootCredentials doesn't require any statements from the user because it's not configurable in any
// way. We simply generate a new password and hit a pre-defined Elasticsearch REST API to rotate them.
func (es *Elasticsearch) RotateRootCredentials(ctx context.Context, _ []string) (map[string]interface{}, error) {
newPassword, err := es.credentialProducer.GeneratePassword()
if err != nil {
return nil, errwrap.Wrapf("unable to generate root password: {{err}}", err)
}
// Don't let anyone read or write the config while we're in the process of rotating the password.
es.mux.Lock()
defer es.mux.Unlock()
client, err := buildClient(es.config)
if err != nil {
return nil, errwrap.Wrapf("unable to get client: {{err}}", err)
}
if err := client.ChangePassword(ctx, es.config["username"].(string), newPassword); err != nil {
return nil, errwrap.Wrapf("unable to change password: {{}}", err)
}
es.config["password"] = newPassword
return es.config, nil
}
func (es *Elasticsearch) Close() error {
// NOOP, nothing to close.
return nil
}
// DEPRECATED, included for backward-compatibility until removal
func (es *Elasticsearch) Initialize(ctx context.Context, config map[string]interface{}, verifyConnection bool) error {
_, err := es.Init(ctx, config, verifyConnection)
return err
}
func newCreationStatement(statements dbplugin.Statements) (*creationStatement, error) {
if len(statements.Creation) == 0 {
return nil, dbutil.ErrEmptyCreationStatement
}
stmt := &creationStatement{}
if err := json.Unmarshal([]byte(statements.Creation[0]), stmt); err != nil {
return nil, errwrap.Wrapf(fmt.Sprintf("unable to unmarshal %s: {{err}}", []byte(statements.Creation[0])), err)
}
if len(stmt.PreexistingRoles) > 0 && len(stmt.RoleToCreate) > 0 {
return nil, errors.New(`"elasticsearch_roles" and "elasticsearch_role_definition" are mutually exclusive`)
}
return stmt, nil
}
type creationStatement struct {
PreexistingRoles []string `json:"elasticsearch_roles"`
RoleToCreate map[string]interface{} `json:"elasticsearch_role_definition"`
}
// buildClient is a helper method for building a client from the present config,
// which is done often.
func buildClient(config map[string]interface{}) (*Client, error) {
// We can presume these required fields are provided by strings
// because they're validated in Init.
clientConfig := &ClientConfig{
Username: config["username"].(string),
Password: config["password"].(string),
BaseURL: config["url"].(string),
}
hasTLSConf := false
tlsConf := &TLSConfig{}
// We can presume that if these are provided, they're in the expected format
// because they're also validated in Init.
if raw, ok := config["ca_cert"]; ok {
tlsConf.CACert = raw.(string)
hasTLSConf = true
}
if raw, ok := config["ca_path"]; ok {
tlsConf.CAPath = raw.(string)
hasTLSConf = true
}
if raw, ok := config["client_cert"]; ok {
tlsConf.ClientCert = raw.(string)
hasTLSConf = true
}
if raw, ok := config["client_key"]; ok {
tlsConf.ClientKey = raw.(string)
hasTLSConf = true
}
if raw, ok := config["tls_server_name"]; ok {
tlsConf.TLSServerName = raw.(string)
hasTLSConf = true
}
if raw, ok := config["insecure"]; ok {
tlsConf.Insecure = raw.(bool)
hasTLSConf = true
}
// We should only fulfill the clientConfig's TLSConfig pointer if we actually
// want the client to use TLS.
if hasTLSConf {
clientConfig.TLSConfig = tlsConf
}
client, err := NewClient(clientConfig)
if err != nil {
return nil, err
}
return client, nil
}
// GenerateCredentials returns a generated password
func (es *Elasticsearch) GenerateCredentials(ctx context.Context) (string, error) {
password, err := es.credentialProducer.GeneratePassword()
if err != nil {
return "", err
}
return password, nil
}
// SetCredentials is used to set the credentials for a database user to a
// specific username and password. This is not currently supported by the
// elastic search plugin, but is needed to conform to the dbplugin.Database
// interface
func (es *Elasticsearch) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticConfig dbplugin.StaticUserConfig) (username string, password string, err error) {
return "", "", dbutil.Unimplemented()
}