2019-01-09 01:26:16 +00:00
|
|
|
package influxdb
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2020-10-12 21:54:26 +00:00
|
|
|
"fmt"
|
2019-01-09 01:26:16 +00:00
|
|
|
"strings"
|
2020-10-12 21:54:26 +00:00
|
|
|
|
2019-01-09 01:26:16 +00:00
|
|
|
multierror "github.com/hashicorp/go-multierror"
|
2021-07-16 00:17:31 +00:00
|
|
|
"github.com/hashicorp/go-secure-stdlib/strutil"
|
2020-10-15 19:20:12 +00:00
|
|
|
dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
|
2019-04-15 18:10:07 +00:00
|
|
|
"github.com/hashicorp/vault/sdk/database/helper/dbutil"
|
2021-06-09 21:08:59 +00:00
|
|
|
"github.com/hashicorp/vault/sdk/helper/template"
|
2019-01-09 01:26:16 +00:00
|
|
|
influx "github.com/influxdata/influxdb/client/v2"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
defaultUserCreationIFQL = `CREATE USER "{{username}}" WITH PASSWORD '{{password}}';`
|
|
|
|
defaultUserDeletionIFQL = `DROP USER "{{username}}";`
|
|
|
|
defaultRootCredentialRotationIFQL = `SET PASSWORD FOR "{{username}}" = '{{password}}';`
|
|
|
|
influxdbTypeName = "influxdb"
|
2021-06-09 21:08:59 +00:00
|
|
|
|
|
|
|
defaultUserNameTemplate = `{{ printf "v_%s_%s_%s_%s" (.DisplayName | truncate 15) (.RoleName | truncate 15) (random 20) (unix_time) | truncate 100 | replace "-" "_" | lowercase }}`
|
2019-01-09 01:26:16 +00:00
|
|
|
)
|
|
|
|
|
2020-10-15 19:20:12 +00:00
|
|
|
var _ dbplugin.Database = &Influxdb{}
|
2019-01-09 01:26:16 +00:00
|
|
|
|
|
|
|
// Influxdb is an implementation of Database interface
|
|
|
|
type Influxdb struct {
|
|
|
|
*influxdbConnectionProducer
|
2021-06-09 21:08:59 +00:00
|
|
|
|
|
|
|
usernameProducer template.StringTemplate
|
2019-01-09 01:26:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// New returns a new Cassandra instance
|
|
|
|
func New() (interface{}, error) {
|
|
|
|
db := new()
|
2020-10-15 19:20:12 +00:00
|
|
|
dbType := dbplugin.NewDatabaseErrorSanitizerMiddleware(db, db.secretValues)
|
2019-01-09 01:26:16 +00:00
|
|
|
|
|
|
|
return dbType, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func new() *Influxdb {
|
|
|
|
connProducer := &influxdbConnectionProducer{}
|
|
|
|
connProducer.Type = influxdbTypeName
|
|
|
|
|
|
|
|
return &Influxdb{
|
|
|
|
influxdbConnectionProducer: connProducer,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Type returns the TypeName for this backend
|
|
|
|
func (i *Influxdb) Type() (string, error) {
|
|
|
|
return influxdbTypeName, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *Influxdb) getConnection(ctx context.Context) (influx.Client, error) {
|
|
|
|
cli, err := i.Connection(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return cli.(influx.Client), nil
|
|
|
|
}
|
|
|
|
|
2021-06-09 21:08:59 +00:00
|
|
|
func (i *Influxdb) Initialize(ctx context.Context, req dbplugin.InitializeRequest) (resp dbplugin.InitializeResponse, err error) {
|
|
|
|
usernameTemplate, err := strutil.GetString(req.Config, "username_template")
|
|
|
|
if err != nil {
|
|
|
|
return dbplugin.InitializeResponse{}, fmt.Errorf("failed to retrieve username_template: %w", err)
|
|
|
|
}
|
|
|
|
if usernameTemplate == "" {
|
|
|
|
usernameTemplate = defaultUserNameTemplate
|
|
|
|
}
|
|
|
|
|
|
|
|
up, err := template.NewTemplate(template.Template(usernameTemplate))
|
|
|
|
if err != nil {
|
|
|
|
return dbplugin.InitializeResponse{}, fmt.Errorf("unable to initialize username template: %w", err)
|
|
|
|
}
|
|
|
|
i.usernameProducer = up
|
|
|
|
|
|
|
|
_, err = i.usernameProducer.Generate(dbplugin.UsernameMetadata{})
|
|
|
|
if err != nil {
|
|
|
|
return dbplugin.InitializeResponse{}, fmt.Errorf("invalid username template: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return i.influxdbConnectionProducer.Initialize(ctx, req)
|
|
|
|
}
|
|
|
|
|
2020-10-12 21:54:26 +00:00
|
|
|
// NewUser generates the username/password on the underlying Influxdb secret backend as instructed by
|
|
|
|
// the statements provided.
|
2020-10-15 19:20:12 +00:00
|
|
|
func (i *Influxdb) NewUser(ctx context.Context, req dbplugin.NewUserRequest) (resp dbplugin.NewUserResponse, err error) {
|
2019-01-09 01:26:16 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
|
|
|
cli, err := i.getConnection(ctx)
|
|
|
|
if err != nil {
|
2020-10-15 19:20:12 +00:00
|
|
|
return dbplugin.NewUserResponse{}, fmt.Errorf("unable to get connection: %w", err)
|
2019-01-09 01:26:16 +00:00
|
|
|
}
|
|
|
|
|
2020-10-12 21:54:26 +00:00
|
|
|
creationIFQL := req.Statements.Commands
|
2019-01-09 01:26:16 +00:00
|
|
|
if len(creationIFQL) == 0 {
|
|
|
|
creationIFQL = []string{defaultUserCreationIFQL}
|
|
|
|
}
|
|
|
|
|
2020-10-12 21:54:26 +00:00
|
|
|
rollbackIFQL := req.RollbackStatements.Commands
|
2019-01-09 01:26:16 +00:00
|
|
|
if len(rollbackIFQL) == 0 {
|
|
|
|
rollbackIFQL = []string{defaultUserDeletionIFQL}
|
|
|
|
}
|
|
|
|
|
2021-06-09 21:08:59 +00:00
|
|
|
username, err := i.usernameProducer.Generate(req.UsernameConfig)
|
2019-01-09 01:26:16 +00:00
|
|
|
if err != nil {
|
2021-06-09 21:08:59 +00:00
|
|
|
return dbplugin.NewUserResponse{}, err
|
2019-01-09 01:26:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, stmt := range creationIFQL {
|
|
|
|
for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") {
|
|
|
|
query = strings.TrimSpace(query)
|
|
|
|
if len(query) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-10-12 21:54:26 +00:00
|
|
|
m := map[string]string{
|
2019-01-09 01:26:16 +00:00
|
|
|
"username": username,
|
2020-10-12 21:54:26 +00:00
|
|
|
"password": req.Password,
|
|
|
|
}
|
2020-12-07 23:18:59 +00:00
|
|
|
qry := influx.NewQuery(dbutil.QueryHelper(query, m), "", "")
|
|
|
|
response, err := cli.Query(qry)
|
|
|
|
// err can be nil with response.Error() being not nil, so both need to be handled
|
|
|
|
merr := multierror.Append(err, response.Error())
|
|
|
|
if merr.ErrorOrNil() != nil {
|
|
|
|
// Attempt rollback only when the response has an error
|
2020-02-05 19:49:02 +00:00
|
|
|
if response != nil && response.Error() != nil {
|
|
|
|
attemptRollback(cli, username, rollbackIFQL)
|
2019-01-09 01:26:16 +00:00
|
|
|
}
|
2020-12-07 23:18:59 +00:00
|
|
|
|
|
|
|
return dbplugin.NewUserResponse{}, fmt.Errorf("failed to run query in InfluxDB: %w", merr)
|
2019-01-09 01:26:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-10-15 19:20:12 +00:00
|
|
|
resp = dbplugin.NewUserResponse{
|
2020-10-12 21:54:26 +00:00
|
|
|
Username: username,
|
|
|
|
}
|
|
|
|
return resp, nil
|
2019-01-09 01:26:16 +00:00
|
|
|
}
|
|
|
|
|
2020-02-05 19:49:02 +00:00
|
|
|
// attemptRollback will attempt to roll back user creation if an error occurs in
|
|
|
|
// CreateUser
|
|
|
|
func attemptRollback(cli influx.Client, username string, rollbackStatements []string) error {
|
|
|
|
for _, stmt := range rollbackStatements {
|
|
|
|
for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") {
|
|
|
|
query = strings.TrimSpace(query)
|
|
|
|
|
|
|
|
if len(query) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
q := influx.NewQuery(dbutil.QueryHelper(query, map[string]string{
|
|
|
|
"username": username,
|
|
|
|
}), "", "")
|
|
|
|
|
|
|
|
response, err := cli.Query(q)
|
2020-12-07 23:18:59 +00:00
|
|
|
// err can be nil with response.Error() being not nil, so both need to be handled
|
|
|
|
merr := multierror.Append(err, response.Error())
|
|
|
|
if merr.ErrorOrNil() != nil {
|
|
|
|
return merr
|
2020-02-05 19:49:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-15 19:20:12 +00:00
|
|
|
func (i *Influxdb) DeleteUser(ctx context.Context, req dbplugin.DeleteUserRequest) (dbplugin.DeleteUserResponse, error) {
|
2019-01-09 01:26:16 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
|
|
|
cli, err := i.getConnection(ctx)
|
|
|
|
if err != nil {
|
2020-10-15 19:20:12 +00:00
|
|
|
return dbplugin.DeleteUserResponse{}, fmt.Errorf("unable to get connection: %w", err)
|
2019-01-09 01:26:16 +00:00
|
|
|
}
|
|
|
|
|
2020-10-12 21:54:26 +00:00
|
|
|
revocationIFQL := req.Statements.Commands
|
2019-01-09 01:26:16 +00:00
|
|
|
if len(revocationIFQL) == 0 {
|
|
|
|
revocationIFQL = []string{defaultUserDeletionIFQL}
|
|
|
|
}
|
|
|
|
|
|
|
|
var result *multierror.Error
|
|
|
|
for _, stmt := range revocationIFQL {
|
|
|
|
for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") {
|
|
|
|
query = strings.TrimSpace(query)
|
|
|
|
if len(query) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
2020-10-12 21:54:26 +00:00
|
|
|
m := map[string]string{
|
|
|
|
"username": req.Username,
|
|
|
|
}
|
|
|
|
q := influx.NewQuery(dbutil.QueryHelper(query, m), "", "")
|
2019-01-09 01:26:16 +00:00
|
|
|
response, err := cli.Query(q)
|
|
|
|
result = multierror.Append(result, err)
|
2020-02-05 19:49:02 +00:00
|
|
|
if response != nil {
|
|
|
|
result = multierror.Append(result, response.Error())
|
|
|
|
}
|
2019-01-09 01:26:16 +00:00
|
|
|
}
|
|
|
|
}
|
2020-10-12 21:54:26 +00:00
|
|
|
if result.ErrorOrNil() != nil {
|
2020-10-15 19:20:12 +00:00
|
|
|
return dbplugin.DeleteUserResponse{}, fmt.Errorf("failed to delete user cleanly: %w", result.ErrorOrNil())
|
2020-10-12 21:54:26 +00:00
|
|
|
}
|
2020-10-15 19:20:12 +00:00
|
|
|
return dbplugin.DeleteUserResponse{}, nil
|
2019-01-09 01:26:16 +00:00
|
|
|
}
|
|
|
|
|
2020-10-15 19:20:12 +00:00
|
|
|
func (i *Influxdb) UpdateUser(ctx context.Context, req dbplugin.UpdateUserRequest) (dbplugin.UpdateUserResponse, error) {
|
2020-10-12 21:54:26 +00:00
|
|
|
if req.Password == nil && req.Expiration == nil {
|
2020-10-15 19:20:12 +00:00
|
|
|
return dbplugin.UpdateUserResponse{}, fmt.Errorf("no changes requested")
|
2020-10-12 21:54:26 +00:00
|
|
|
}
|
|
|
|
|
2019-01-09 01:26:16 +00:00
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
2020-10-12 21:54:26 +00:00
|
|
|
if req.Password != nil {
|
|
|
|
err := i.changeUserPassword(ctx, req.Username, req.Password)
|
|
|
|
if err != nil {
|
2020-10-15 19:20:12 +00:00
|
|
|
return dbplugin.UpdateUserResponse{}, fmt.Errorf("failed to change %q password: %w", req.Username, err)
|
2020-10-12 21:54:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Expiration is a no-op
|
2020-10-15 19:20:12 +00:00
|
|
|
return dbplugin.UpdateUserResponse{}, nil
|
2020-10-12 21:54:26 +00:00
|
|
|
}
|
|
|
|
|
2020-10-15 19:20:12 +00:00
|
|
|
func (i *Influxdb) changeUserPassword(ctx context.Context, username string, changePassword *dbplugin.ChangePassword) error {
|
2019-01-09 01:26:16 +00:00
|
|
|
cli, err := i.getConnection(ctx)
|
|
|
|
if err != nil {
|
2020-10-12 21:54:26 +00:00
|
|
|
return fmt.Errorf("unable to get connection: %w", err)
|
2019-01-09 01:26:16 +00:00
|
|
|
}
|
|
|
|
|
2020-10-12 21:54:26 +00:00
|
|
|
rotateIFQL := changePassword.Statements.Commands
|
2019-01-09 01:26:16 +00:00
|
|
|
if len(rotateIFQL) == 0 {
|
|
|
|
rotateIFQL = []string{defaultRootCredentialRotationIFQL}
|
|
|
|
}
|
|
|
|
|
|
|
|
var result *multierror.Error
|
|
|
|
for _, stmt := range rotateIFQL {
|
|
|
|
for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") {
|
|
|
|
query = strings.TrimSpace(query)
|
|
|
|
if len(query) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
2020-10-12 21:54:26 +00:00
|
|
|
m := map[string]string{
|
|
|
|
"username": username,
|
|
|
|
"password": changePassword.NewPassword,
|
|
|
|
}
|
|
|
|
q := influx.NewQuery(dbutil.QueryHelper(query, m), "", "")
|
2019-01-09 01:26:16 +00:00
|
|
|
response, err := cli.Query(q)
|
|
|
|
result = multierror.Append(result, err)
|
2020-02-05 19:49:02 +00:00
|
|
|
if response != nil {
|
|
|
|
result = multierror.Append(result, response.Error())
|
|
|
|
}
|
2019-01-09 01:26:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
err = result.ErrorOrNil()
|
|
|
|
if err != nil {
|
2020-10-12 21:54:26 +00:00
|
|
|
return fmt.Errorf("failed to execute rotation queries: %w", err)
|
2019-01-09 01:26:16 +00:00
|
|
|
}
|
|
|
|
|
2020-10-12 21:54:26 +00:00
|
|
|
return nil
|
2019-01-09 01:26:16 +00:00
|
|
|
}
|