1package influxdb
2
3import (
4	"context"
5	"strings"
6	"time"
7
8	multierror "github.com/hashicorp/go-multierror"
9	"github.com/hashicorp/vault/api"
10	"github.com/hashicorp/vault/sdk/database/dbplugin"
11	"github.com/hashicorp/vault/sdk/database/helper/credsutil"
12	"github.com/hashicorp/vault/sdk/database/helper/dbutil"
13	"github.com/hashicorp/vault/sdk/helper/strutil"
14	influx "github.com/influxdata/influxdb/client/v2"
15)
16
17const (
18	defaultUserCreationIFQL           = `CREATE USER "{{username}}" WITH PASSWORD '{{password}}';`
19	defaultUserDeletionIFQL           = `DROP USER "{{username}}";`
20	defaultRootCredentialRotationIFQL = `SET PASSWORD FOR "{{username}}" = '{{password}}';`
21	influxdbTypeName                  = "influxdb"
22)
23
24var _ dbplugin.Database = &Influxdb{}
25
26// Influxdb is an implementation of Database interface
27type Influxdb struct {
28	*influxdbConnectionProducer
29	credsutil.CredentialsProducer
30}
31
32// New returns a new Cassandra instance
33func New() (interface{}, error) {
34	db := new()
35	dbType := dbplugin.NewDatabaseErrorSanitizerMiddleware(db, db.secretValues)
36
37	return dbType, nil
38}
39
40func new() *Influxdb {
41	connProducer := &influxdbConnectionProducer{}
42	connProducer.Type = influxdbTypeName
43
44	credsProducer := &credsutil.SQLCredentialsProducer{
45		DisplayNameLen: 15,
46		RoleNameLen:    15,
47		UsernameLen:    100,
48		Separator:      "_",
49	}
50
51	return &Influxdb{
52		influxdbConnectionProducer: connProducer,
53		CredentialsProducer:        credsProducer,
54	}
55}
56
57// Run instantiates a Influxdb object, and runs the RPC server for the plugin
58func Run(apiTLSConfig *api.TLSConfig) error {
59	dbType, err := New()
60	if err != nil {
61		return err
62	}
63
64	dbplugin.Serve(dbType.(dbplugin.Database), api.VaultPluginTLSProvider(apiTLSConfig))
65
66	return nil
67}
68
69// Type returns the TypeName for this backend
70func (i *Influxdb) Type() (string, error) {
71	return influxdbTypeName, nil
72}
73
74func (i *Influxdb) getConnection(ctx context.Context) (influx.Client, error) {
75	cli, err := i.Connection(ctx)
76	if err != nil {
77		return nil, err
78	}
79
80	return cli.(influx.Client), nil
81}
82
83// CreateUser generates the username/password on the underlying Influxdb secret backend as instructed by
84// the CreationStatement provided.
85func (i *Influxdb) CreateUser(ctx context.Context, statements dbplugin.Statements, usernameConfig dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) {
86	// Grab the lock
87	i.Lock()
88	defer i.Unlock()
89
90	statements = dbutil.StatementCompatibilityHelper(statements)
91
92	// Get the connection
93	cli, err := i.getConnection(ctx)
94	if err != nil {
95		return "", "", err
96	}
97
98	creationIFQL := statements.Creation
99	if len(creationIFQL) == 0 {
100		creationIFQL = []string{defaultUserCreationIFQL}
101	}
102
103	rollbackIFQL := statements.Rollback
104	if len(rollbackIFQL) == 0 {
105		rollbackIFQL = []string{defaultUserDeletionIFQL}
106	}
107
108	username, err = i.GenerateUsername(usernameConfig)
109	username = strings.Replace(username, "-", "_", -1)
110	if err != nil {
111		return "", "", err
112	}
113	username = strings.ToLower(username)
114	password, err = i.GeneratePassword()
115	if err != nil {
116		return "", "", err
117	}
118
119	// Execute each query
120	for _, stmt := range creationIFQL {
121		for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") {
122			query = strings.TrimSpace(query)
123			if len(query) == 0 {
124				continue
125			}
126
127			q := influx.NewQuery(dbutil.QueryHelper(query, map[string]string{
128				"username": username,
129				"password": password,
130			}), "", "")
131			response, err := cli.Query(q)
132			if err != nil && response.Error() != nil {
133				for _, stmt := range rollbackIFQL {
134					for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") {
135						query = strings.TrimSpace(query)
136
137						if len(query) == 0 {
138							continue
139						}
140						q := influx.NewQuery(dbutil.QueryHelper(query, map[string]string{
141							"username": username,
142						}), "", "")
143
144						response, err := cli.Query(q)
145						if err != nil && response.Error() != nil {
146							return "", "", err
147						}
148					}
149				}
150				return "", "", err
151			}
152		}
153	}
154	return username, password, nil
155}
156
157// RenewUser is not supported on Influxdb, so this is a no-op.
158func (i *Influxdb) RenewUser(ctx context.Context, statements dbplugin.Statements, username string, expiration time.Time) error {
159	// NOOP
160	return nil
161}
162
163// RevokeUser attempts to drop the specified user.
164func (i *Influxdb) RevokeUser(ctx context.Context, statements dbplugin.Statements, username string) error {
165	// Grab the lock
166	i.Lock()
167	defer i.Unlock()
168
169	statements = dbutil.StatementCompatibilityHelper(statements)
170
171	cli, err := i.getConnection(ctx)
172	if err != nil {
173		return err
174	}
175
176	revocationIFQL := statements.Revocation
177	if len(revocationIFQL) == 0 {
178		revocationIFQL = []string{defaultUserDeletionIFQL}
179	}
180
181	var result *multierror.Error
182	for _, stmt := range revocationIFQL {
183		for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") {
184			query = strings.TrimSpace(query)
185			if len(query) == 0 {
186				continue
187			}
188			q := influx.NewQuery(dbutil.QueryHelper(query, map[string]string{
189				"username": username,
190			}), "", "")
191			response, err := cli.Query(q)
192			result = multierror.Append(result, err)
193			result = multierror.Append(result, response.Error())
194		}
195	}
196	return result.ErrorOrNil()
197}
198
199// RotateRootCredentials is useful when we try to change root credential
200func (i *Influxdb) RotateRootCredentials(ctx context.Context, statements []string) (map[string]interface{}, error) {
201	// Grab the lock
202	i.Lock()
203	defer i.Unlock()
204
205	cli, err := i.getConnection(ctx)
206	if err != nil {
207		return nil, err
208	}
209
210	rotateIFQL := statements
211	if len(rotateIFQL) == 0 {
212		rotateIFQL = []string{defaultRootCredentialRotationIFQL}
213	}
214
215	password, err := i.GeneratePassword()
216	if err != nil {
217		return nil, err
218	}
219
220	var result *multierror.Error
221	for _, stmt := range rotateIFQL {
222		for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") {
223			query = strings.TrimSpace(query)
224			if len(query) == 0 {
225				continue
226			}
227			q := influx.NewQuery(dbutil.QueryHelper(query, map[string]string{
228				"username": i.Username,
229				"password": password,
230			}), "", "")
231			response, err := cli.Query(q)
232			result = multierror.Append(result, err)
233			result = multierror.Append(result, response.Error())
234		}
235	}
236
237	err = result.ErrorOrNil()
238	if err != nil {
239		return nil, err
240	}
241
242	i.rawConfig["password"] = password
243	return i.rawConfig, nil
244}
245