1package hana
2
3import (
4	"context"
5	"database/sql"
6	"errors"
7	"fmt"
8	"strings"
9	"time"
10
11	_ "github.com/SAP/go-hdb/driver"
12	"github.com/hashicorp/vault/api"
13	"github.com/hashicorp/vault/sdk/database/dbplugin"
14	"github.com/hashicorp/vault/sdk/database/helper/connutil"
15	"github.com/hashicorp/vault/sdk/database/helper/credsutil"
16	"github.com/hashicorp/vault/sdk/database/helper/dbutil"
17	"github.com/hashicorp/vault/sdk/helper/dbtxn"
18	"github.com/hashicorp/vault/sdk/helper/strutil"
19)
20
21const (
22	hanaTypeName = "hdb"
23)
24
25// HANA is an implementation of Database interface
26type HANA struct {
27	*connutil.SQLConnectionProducer
28	credsutil.CredentialsProducer
29}
30
31var _ dbplugin.Database = &HANA{}
32
33// New implements builtinplugins.BuiltinFactory
34func New() (interface{}, error) {
35	db := new()
36	// Wrap the plugin with middleware to sanitize errors
37	dbType := dbplugin.NewDatabaseErrorSanitizerMiddleware(db, db.SecretValues)
38
39	return dbType, nil
40}
41
42func new() *HANA {
43	connProducer := &connutil.SQLConnectionProducer{}
44	connProducer.Type = hanaTypeName
45
46	credsProducer := &credsutil.SQLCredentialsProducer{
47		DisplayNameLen: 32,
48		RoleNameLen:    20,
49		UsernameLen:    128,
50		Separator:      "_",
51	}
52
53	return &HANA{
54		SQLConnectionProducer: connProducer,
55		CredentialsProducer:   credsProducer,
56	}
57}
58
59// Run instantiates a HANA object, and runs the RPC server for the plugin
60func Run(apiTLSConfig *api.TLSConfig) error {
61	dbType, err := New()
62	if err != nil {
63		return err
64	}
65
66	dbplugin.Serve(dbType.(dbplugin.Database), api.VaultPluginTLSProvider(apiTLSConfig))
67
68	return nil
69}
70
71// Type returns the TypeName for this backend
72func (h *HANA) Type() (string, error) {
73	return hanaTypeName, nil
74}
75
76func (h *HANA) getConnection(ctx context.Context) (*sql.DB, error) {
77	db, err := h.Connection(ctx)
78	if err != nil {
79		return nil, err
80	}
81
82	return db.(*sql.DB), nil
83}
84
85// CreateUser generates the username/password on the underlying HANA secret backend
86// as instructed by the CreationStatement provided.
87func (h *HANA) CreateUser(ctx context.Context, statements dbplugin.Statements, usernameConfig dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) {
88	// Grab the lock
89	h.Lock()
90	defer h.Unlock()
91
92	statements = dbutil.StatementCompatibilityHelper(statements)
93
94	// Get the connection
95	db, err := h.getConnection(ctx)
96	if err != nil {
97		return "", "", err
98	}
99
100	if len(statements.Creation) == 0 {
101		return "", "", dbutil.ErrEmptyCreationStatement
102	}
103
104	// Generate username
105	username, err = h.GenerateUsername(usernameConfig)
106	if err != nil {
107		return "", "", err
108	}
109
110	// HANA does not allow hyphens in usernames, and highly prefers capital letters
111	username = strings.Replace(username, "-", "_", -1)
112	username = strings.ToUpper(username)
113
114	// Generate password
115	password, err = h.GeneratePassword()
116	if err != nil {
117		return "", "", err
118	}
119	// Most HANA configurations have password constraints
120	// Prefix with A1a to satisfy these constraints. User will be forced to change upon login
121	password = strings.Replace(password, "-", "_", -1)
122	password = "A1a" + password
123
124	// If expiration is in the role SQL, HANA will deactivate the user when time is up,
125	// regardless of whether vault is alive to revoke lease
126	expirationStr, err := h.GenerateExpiration(expiration)
127	if err != nil {
128		return "", "", err
129	}
130
131	// Start a transaction
132	tx, err := db.BeginTx(ctx, nil)
133	if err != nil {
134		return "", "", err
135	}
136	defer tx.Rollback()
137
138	// Execute each query
139	for _, stmt := range statements.Creation {
140		for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") {
141			query = strings.TrimSpace(query)
142			if len(query) == 0 {
143				continue
144			}
145
146			m := map[string]string{
147				"name":       username,
148				"password":   password,
149				"expiration": expirationStr,
150			}
151			if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
152				return "", "", err
153			}
154		}
155	}
156
157	// Commit the transaction
158	if err := tx.Commit(); err != nil {
159		return "", "", err
160	}
161
162	return username, password, nil
163}
164
165// Renewing hana user just means altering user's valid until property
166func (h *HANA) RenewUser(ctx context.Context, statements dbplugin.Statements, username string, expiration time.Time) error {
167	statements = dbutil.StatementCompatibilityHelper(statements)
168
169	// Get connection
170	db, err := h.getConnection(ctx)
171	if err != nil {
172		return err
173	}
174
175	// Start a transaction
176	tx, err := db.BeginTx(ctx, nil)
177	if err != nil {
178		return err
179	}
180	defer tx.Rollback()
181
182	// If expiration is in the role SQL, HANA will deactivate the user when time is up,
183	// regardless of whether vault is alive to revoke lease
184	expirationStr, err := h.GenerateExpiration(expiration)
185	if err != nil {
186		return err
187	}
188
189	// Renew user's valid until property field
190	stmt, err := tx.PrepareContext(ctx, "ALTER USER "+username+" VALID UNTIL "+"'"+expirationStr+"'")
191	if err != nil {
192		return err
193	}
194	defer stmt.Close()
195	if _, err := stmt.ExecContext(ctx); err != nil {
196		return err
197	}
198
199	// Commit the transaction
200	if err := tx.Commit(); err != nil {
201		return err
202	}
203
204	return nil
205}
206
207// Revoking hana user will deactivate user and try to perform a soft drop
208func (h *HANA) RevokeUser(ctx context.Context, statements dbplugin.Statements, username string) error {
209	statements = dbutil.StatementCompatibilityHelper(statements)
210
211	// default revoke will be a soft drop on user
212	if len(statements.Revocation) == 0 {
213		return h.revokeUserDefault(ctx, username)
214	}
215
216	// Get connection
217	db, err := h.getConnection(ctx)
218	if err != nil {
219		return err
220	}
221
222	// Start a transaction
223	tx, err := db.BeginTx(ctx, nil)
224	if err != nil {
225		return err
226	}
227	defer tx.Rollback()
228
229	// Execute each query
230	for _, stmt := range statements.Revocation {
231		for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") {
232			query = strings.TrimSpace(query)
233			if len(query) == 0 {
234				continue
235			}
236
237			m := map[string]string{
238				"name": username,
239			}
240			if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
241				return err
242			}
243		}
244	}
245
246	return tx.Commit()
247}
248
249func (h *HANA) revokeUserDefault(ctx context.Context, username string) error {
250	// Get connection
251	db, err := h.getConnection(ctx)
252	if err != nil {
253		return err
254	}
255
256	// Start a transaction
257	tx, err := db.BeginTx(ctx, nil)
258	if err != nil {
259		return err
260	}
261	defer tx.Rollback()
262
263	// Disable server login for user
264	disableStmt, err := tx.PrepareContext(ctx, fmt.Sprintf("ALTER USER %s DEACTIVATE USER NOW", username))
265	if err != nil {
266		return err
267	}
268	defer disableStmt.Close()
269	if _, err := disableStmt.ExecContext(ctx); err != nil {
270		return err
271	}
272
273	// Invalidates current sessions and performs soft drop (drop if no dependencies)
274	// if hard drop is desired, custom revoke statements should be written for role
275	dropStmt, err := tx.PrepareContext(ctx, fmt.Sprintf("DROP USER %s RESTRICT", username))
276	if err != nil {
277		return err
278	}
279	defer dropStmt.Close()
280	if _, err := dropStmt.ExecContext(ctx); err != nil {
281		return err
282	}
283
284	// Commit transaction
285	if err := tx.Commit(); err != nil {
286		return err
287	}
288
289	return nil
290}
291
292// RotateRootCredentials is not currently supported on HANA
293func (h *HANA) RotateRootCredentials(ctx context.Context, statements []string) (map[string]interface{}, error) {
294	return nil, errors.New("root credentaion rotation is not currently implemented in this database secrets engine")
295}
296