open-vault/plugins/database/influxdb/influxdb_test.go

485 lines
14 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package influxdb
import (
"context"
"fmt"
"net/url"
"os"
"reflect"
"strconv"
"strings"
"testing"
"time"
"github.com/hashicorp/vault/sdk/database/dbplugin/v5"
dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing"
"github.com/hashicorp/vault/sdk/helper/docker"
influx "github.com/influxdata/influxdb1-client/v2"
"github.com/stretchr/testify/require"
)
const createUserStatements = `CREATE USER "{{username}}" WITH PASSWORD '{{password}}';GRANT ALL ON "vault" TO "{{username}}";`
type Config struct {
docker.ServiceURL
Username string
Password string
}
var _ docker.ServiceConfig = &Config{}
func (c *Config) apiConfig() influx.HTTPConfig {
return influx.HTTPConfig{
Addr: c.URL().String(),
Username: c.Username,
Password: c.Password,
}
}
func (c *Config) connectionParams() map[string]interface{} {
pieces := strings.Split(c.Address(), ":")
port, _ := strconv.Atoi(pieces[1])
return map[string]interface{}{
"host": pieces[0],
"port": port,
"username": c.Username,
"password": c.Password,
}
}
func prepareInfluxdbTestContainer(t *testing.T) (func(), *Config) {
c := &Config{
Username: "influx-root",
Password: "influx-root",
}
if host := os.Getenv("INFLUXDB_HOST"); host != "" {
c.ServiceURL = *docker.NewServiceURL(url.URL{Scheme: "http", Host: host})
return func() {}, c
}
runner, err := docker.NewServiceRunner(docker.RunOptions{
ImageRepo: "docker.mirror.hashicorp.services/influxdb",
ContainerName: "influxdb",
ImageTag: "1.8-alpine",
Env: []string{
"INFLUXDB_DB=vault",
"INFLUXDB_ADMIN_USER=" + c.Username,
"INFLUXDB_ADMIN_PASSWORD=" + c.Password,
"INFLUXDB_HTTP_AUTH_ENABLED=true",
},
Ports: []string{"8086/tcp"},
})
if err != nil {
t.Fatalf("Could not start docker InfluxDB: %s", err)
}
svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) {
c.ServiceURL = *docker.NewServiceURL(url.URL{
Scheme: "http",
Host: fmt.Sprintf("%s:%d", host, port),
})
cli, err := influx.NewHTTPClient(c.apiConfig())
if err != nil {
return nil, fmt.Errorf("error creating InfluxDB client: %w", err)
}
defer cli.Close()
_, _, err = cli.Ping(1)
if err != nil {
return nil, fmt.Errorf("error checking cluster status: %w", err)
}
return c, nil
})
if err != nil {
t.Fatalf("Could not start docker InfluxDB: %s", err)
}
return svc.Cleanup, svc.Config.(*Config)
}
func TestInfluxdb_Initialize(t *testing.T) {
cleanup, config := prepareInfluxdbTestContainer(t)
defer cleanup()
type testCase struct {
req dbplugin.InitializeRequest
expectedResponse dbplugin.InitializeResponse
expectErr bool
expectInitialized bool
}
tests := map[string]testCase{
"port is an int": {
req: dbplugin.InitializeRequest{
Config: makeConfig(config.connectionParams()),
VerifyConnection: true,
},
expectedResponse: dbplugin.InitializeResponse{
Config: config.connectionParams(),
},
expectErr: false,
expectInitialized: true,
},
"port is a string": {
req: dbplugin.InitializeRequest{
Config: makeConfig(config.connectionParams(), "port", strconv.Itoa(config.connectionParams()["port"].(int))),
VerifyConnection: true,
},
expectedResponse: dbplugin.InitializeResponse{
Config: makeConfig(config.connectionParams(), "port", strconv.Itoa(config.connectionParams()["port"].(int))),
},
expectErr: false,
expectInitialized: true,
},
"missing config": {
req: dbplugin.InitializeRequest{
Config: nil,
VerifyConnection: true,
},
expectedResponse: dbplugin.InitializeResponse{},
expectErr: true,
expectInitialized: false,
},
"missing host": {
req: dbplugin.InitializeRequest{
Config: makeConfig(config.connectionParams(), "host", ""),
VerifyConnection: true,
},
expectedResponse: dbplugin.InitializeResponse{},
expectErr: true,
expectInitialized: false,
},
"missing username": {
req: dbplugin.InitializeRequest{
Config: makeConfig(config.connectionParams(), "username", ""),
VerifyConnection: true,
},
expectedResponse: dbplugin.InitializeResponse{},
expectErr: true,
expectInitialized: false,
},
"missing password": {
req: dbplugin.InitializeRequest{
Config: makeConfig(config.connectionParams(), "password", ""),
VerifyConnection: true,
},
expectedResponse: dbplugin.InitializeResponse{},
expectErr: true,
expectInitialized: false,
},
"failed to validate connection": {
req: dbplugin.InitializeRequest{
// Host exists, but isn't a running instance
Config: makeConfig(config.connectionParams(), "host", "foobar://bad_connection"),
VerifyConnection: true,
},
expectedResponse: dbplugin.InitializeResponse{},
expectErr: true,
expectInitialized: true,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
db := new()
defer dbtesting.AssertClose(t, db)
resp, err := db.Initialize(context.Background(), test.req)
if test.expectErr && err == nil {
t.Fatalf("err expected, got nil")
}
if !test.expectErr && err != nil {
t.Fatalf("no error expected, got: %s", err)
}
if !reflect.DeepEqual(resp, test.expectedResponse) {
t.Fatalf("Actual response: %#v\nExpected response: %#v", resp, test.expectedResponse)
}
if test.expectInitialized && !db.Initialized {
t.Fatalf("Database should be initialized but wasn't")
} else if !test.expectInitialized && db.Initialized {
t.Fatalf("Database was initiailized when it shouldn't")
}
})
}
}
func makeConfig(rootConfig map[string]interface{}, keyValues ...interface{}) map[string]interface{} {
if len(keyValues)%2 != 0 {
panic("makeConfig must be provided with key and value pairs")
}
// Make a copy of the map so there isn't a chance of test bleedover between maps
config := make(map[string]interface{}, len(rootConfig)+(len(keyValues)/2))
for k, v := range rootConfig {
config[k] = v
}
for i := 0; i < len(keyValues); i += 2 {
k := keyValues[i].(string) // Will panic if the key field isn't a string and that's fine in a test
v := keyValues[i+1]
config[k] = v
}
return config
}
func TestInfluxdb_CreateUser_DefaultUsernameTemplate(t *testing.T) {
cleanup, config := prepareInfluxdbTestContainer(t)
defer cleanup()
db := new()
req := dbplugin.InitializeRequest{
Config: config.connectionParams(),
VerifyConnection: true,
}
dbtesting.AssertInitialize(t, db, req)
password := "nuozxby98523u89bdfnkjl"
newUserReq := dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: "token",
RoleName: "mylongrolenamewithmanycharacters",
},
Statements: dbplugin.Statements{
Commands: []string{createUserStatements},
},
Password: password,
Expiration: time.Now().Add(1 * time.Minute),
}
resp := dbtesting.AssertNewUser(t, db, newUserReq)
if resp.Username == "" {
t.Fatalf("Missing username")
}
assertCredsExist(t, config.URL().String(), resp.Username, password)
require.Regexp(t, `^v_token_mylongrolenamew_[a-z0-9]{20}_[0-9]{10}$`, resp.Username)
}
func TestInfluxdb_CreateUser_CustomUsernameTemplate(t *testing.T) {
cleanup, config := prepareInfluxdbTestContainer(t)
defer cleanup()
db := new()
conf := config.connectionParams()
conf["username_template"] = "{{.DisplayName}}_{{random 10}}"
req := dbplugin.InitializeRequest{
Config: conf,
VerifyConnection: true,
}
dbtesting.AssertInitialize(t, db, req)
password := "nuozxby98523u89bdfnkjl"
newUserReq := dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: "token",
RoleName: "mylongrolenamewithmanycharacters",
},
Statements: dbplugin.Statements{
Commands: []string{createUserStatements},
},
Password: password,
Expiration: time.Now().Add(1 * time.Minute),
}
resp := dbtesting.AssertNewUser(t, db, newUserReq)
if resp.Username == "" {
t.Fatalf("Missing username")
}
assertCredsExist(t, config.URL().String(), resp.Username, password)
require.Regexp(t, `^token_[a-zA-Z0-9]{10}$`, resp.Username)
}
func TestUpdateUser_expiration(t *testing.T) {
// This test should end up with a no-op since the expiration doesn't do anything in Influx
cleanup, config := prepareInfluxdbTestContainer(t)
defer cleanup()
db := new()
req := dbplugin.InitializeRequest{
Config: config.connectionParams(),
VerifyConnection: true,
}
dbtesting.AssertInitialize(t, db, req)
password := "nuozxby98523u89bdfnkjl"
newUserReq := dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: "test",
RoleName: "test",
},
Statements: dbplugin.Statements{
Commands: []string{createUserStatements},
},
Password: password,
Expiration: time.Now().Add(1 * time.Minute),
}
newUserResp := dbtesting.AssertNewUser(t, db, newUserReq)
assertCredsExist(t, config.URL().String(), newUserResp.Username, password)
renewReq := dbplugin.UpdateUserRequest{
Username: newUserResp.Username,
Expiration: &dbplugin.ChangeExpiration{
NewExpiration: time.Now().Add(5 * time.Minute),
},
}
dbtesting.AssertUpdateUser(t, db, renewReq)
// Make sure the user hasn't changed
assertCredsExist(t, config.URL().String(), newUserResp.Username, password)
}
func TestUpdateUser_password(t *testing.T) {
cleanup, config := prepareInfluxdbTestContainer(t)
defer cleanup()
db := new()
req := dbplugin.InitializeRequest{
Config: config.connectionParams(),
VerifyConnection: true,
}
dbtesting.AssertInitialize(t, db, req)
initialPassword := "nuozxby98523u89bdfnkjl"
newUserReq := dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: "test",
RoleName: "test",
},
Statements: dbplugin.Statements{
Commands: []string{createUserStatements},
},
Password: initialPassword,
Expiration: time.Now().Add(1 * time.Minute),
}
newUserResp := dbtesting.AssertNewUser(t, db, newUserReq)
assertCredsExist(t, config.URL().String(), newUserResp.Username, initialPassword)
newPassword := "y89qgmbzadiygry8uazodijnb"
newPasswordReq := dbplugin.UpdateUserRequest{
Username: newUserResp.Username,
Password: &dbplugin.ChangePassword{
NewPassword: newPassword,
},
}
dbtesting.AssertUpdateUser(t, db, newPasswordReq)
assertCredsDoNotExist(t, config.URL().String(), newUserResp.Username, initialPassword)
assertCredsExist(t, config.URL().String(), newUserResp.Username, newPassword)
}
// TestInfluxdb_RevokeDeletedUser tests attempting to revoke a user that was
// deleted externally. Guards against a panic, see
// https://github.com/hashicorp/vault/issues/6734
// Updated to attempt to delete a user that never existed to replicate a similar scenario since
// the cleanup function from `prepareInfluxdbTestContainer` does not do anything if using an
// external InfluxDB instance rather than spinning one up for the test.
func TestInfluxdb_RevokeDeletedUser(t *testing.T) {
cleanup, config := prepareInfluxdbTestContainer(t)
defer cleanup()
db := new()
req := dbplugin.InitializeRequest{
Config: config.connectionParams(),
VerifyConnection: true,
}
dbtesting.AssertInitialize(t, db, req)
// attempt to revoke a user that does not exist
delReq := dbplugin.DeleteUserRequest{
Username: "someuser",
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := db.DeleteUser(ctx, delReq)
if err == nil {
t.Fatalf("Expected err, got nil")
}
}
func TestInfluxdb_RevokeUser(t *testing.T) {
cleanup, config := prepareInfluxdbTestContainer(t)
defer cleanup()
db := new()
req := dbplugin.InitializeRequest{
Config: config.connectionParams(),
VerifyConnection: true,
}
dbtesting.AssertInitialize(t, db, req)
initialPassword := "nuozxby98523u89bdfnkjl"
newUserReq := dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: "test",
RoleName: "test",
},
Statements: dbplugin.Statements{
Commands: []string{createUserStatements},
},
Password: initialPassword,
Expiration: time.Now().Add(1 * time.Minute),
}
newUserResp := dbtesting.AssertNewUser(t, db, newUserReq)
assertCredsExist(t, config.URL().String(), newUserResp.Username, initialPassword)
delReq := dbplugin.DeleteUserRequest{
Username: newUserResp.Username,
}
dbtesting.AssertDeleteUser(t, db, delReq)
assertCredsDoNotExist(t, config.URL().String(), newUserResp.Username, initialPassword)
}
func assertCredsExist(t testing.TB, address, username, password string) {
t.Helper()
err := testCredsExist(address, username, password)
if err != nil {
t.Fatalf("Could not log in as %q", username)
}
}
func assertCredsDoNotExist(t testing.TB, address, username, password string) {
t.Helper()
err := testCredsExist(address, username, password)
if err == nil {
t.Fatalf("Able to log in as %q when it shouldn't", username)
}
}
func testCredsExist(address, username, password string) error {
conf := influx.HTTPConfig{
Addr: address,
Username: username,
Password: password,
}
cli, err := influx.NewHTTPClient(conf)
if err != nil {
return fmt.Errorf("Error creating InfluxDB Client: %w", err)
}
defer cli.Close()
_, _, err = cli.Ping(1)
if err != nil {
return fmt.Errorf("error checking server ping: %w", err)
}
q := influx.NewQuery("SHOW SERIES ON vault", "", "")
response, err := cli.Query(q)
if err != nil {
return fmt.Errorf("error querying influxdb server: %w", err)
}
if response != nil && response.Error() != nil {
return fmt.Errorf("error using the correct influx database: %w", response.Error())
}
return nil
}