// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package hana import ( "context" "database/sql" "fmt" "strings" _ "github.com/SAP/go-hdb/driver" "github.com/hashicorp/go-secure-stdlib/strutil" "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "github.com/hashicorp/vault/sdk/database/helper/connutil" "github.com/hashicorp/vault/sdk/database/helper/dbutil" "github.com/hashicorp/vault/sdk/helper/dbtxn" "github.com/hashicorp/vault/sdk/helper/template" ) const ( hanaTypeName = "hdb" defaultUserNameTemplate = `{{ printf "v_%s_%s_%s_%s" (.DisplayName | truncate 32) (.RoleName | truncate 20) (random 20) (unix_time) | truncate 127 | replace "-" "_" | uppercase }}` ) // HANA is an implementation of Database interface type HANA struct { *connutil.SQLConnectionProducer usernameProducer template.StringTemplate } var _ dbplugin.Database = (*HANA)(nil) // New implements builtinplugins.BuiltinFactory func New() (interface{}, error) { db := new() // Wrap the plugin with middleware to sanitize errors dbType := dbplugin.NewDatabaseErrorSanitizerMiddleware(db, db.secretValues) return dbType, nil } func new() *HANA { connProducer := &connutil.SQLConnectionProducer{} connProducer.Type = hanaTypeName return &HANA{ SQLConnectionProducer: connProducer, } } func (h *HANA) secretValues() map[string]string { return map[string]string{ h.Password: "[password]", } } func (h *HANA) Initialize(ctx context.Context, req dbplugin.InitializeRequest) (dbplugin.InitializeResponse, error) { conf, err := h.Init(ctx, req.Config, req.VerifyConnection) if err != nil { return dbplugin.InitializeResponse{}, fmt.Errorf("error initializing db: %w", err) } 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) } h.usernameProducer = up _, err = h.usernameProducer.Generate(dbplugin.UsernameMetadata{}) if err != nil { return dbplugin.InitializeResponse{}, fmt.Errorf("invalid username template: %w", err) } return dbplugin.InitializeResponse{ Config: conf, }, nil } // Type returns the TypeName for this backend func (h *HANA) Type() (string, error) { return hanaTypeName, nil } func (h *HANA) getConnection(ctx context.Context) (*sql.DB, error) { db, err := h.Connection(ctx) if err != nil { return nil, err } return db.(*sql.DB), nil } // NewUser generates the username/password on the underlying HANA secret backend // as instructed by the CreationStatement provided. func (h *HANA) NewUser(ctx context.Context, req dbplugin.NewUserRequest) (response dbplugin.NewUserResponse, err error) { // Grab the lock h.Lock() defer h.Unlock() // Get the connection db, err := h.getConnection(ctx) if err != nil { return dbplugin.NewUserResponse{}, err } if len(req.Statements.Commands) == 0 { return dbplugin.NewUserResponse{}, dbutil.ErrEmptyCreationStatement } // Generate username username, err := h.usernameProducer.Generate(req.UsernameConfig) if err != nil { return dbplugin.NewUserResponse{}, err } // HANA does not allow hyphens in usernames, and highly prefers capital letters username = strings.ReplaceAll(username, "-", "_") username = strings.ToUpper(username) // If expiration is in the role SQL, HANA will deactivate the user when time is up, // regardless of whether vault is alive to revoke lease expirationStr := req.Expiration.UTC().Format("2006-01-02 15:04:05") // Start a transaction tx, err := db.BeginTx(ctx, nil) if err != nil { return dbplugin.NewUserResponse{}, err } defer tx.Rollback() // Execute each query for _, stmt := range req.Statements.Commands { for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") { query = strings.TrimSpace(query) if len(query) == 0 { continue } m := map[string]string{ "name": username, "password": req.Password, "expiration": expirationStr, } if err := dbtxn.ExecuteTxQueryDirect(ctx, tx, m, query); err != nil { return dbplugin.NewUserResponse{}, err } } } // Commit the transaction if err := tx.Commit(); err != nil { return dbplugin.NewUserResponse{}, err } resp := dbplugin.NewUserResponse{ Username: username, } return resp, nil } // UpdateUser allows for updating the expiration or password of the user mentioned in // the UpdateUserRequest func (h *HANA) UpdateUser(ctx context.Context, req dbplugin.UpdateUserRequest) (dbplugin.UpdateUserResponse, error) { h.Lock() defer h.Unlock() // No change requested if req.Password == nil && req.Expiration == nil { return dbplugin.UpdateUserResponse{}, nil } // Get connection db, err := h.getConnection(ctx) if err != nil { return dbplugin.UpdateUserResponse{}, err } // Start a transaction tx, err := db.BeginTx(ctx, nil) if err != nil { return dbplugin.UpdateUserResponse{}, err } defer tx.Rollback() if req.Password != nil { err = h.updateUserPassword(ctx, tx, req.Username, req.Password) if err != nil { return dbplugin.UpdateUserResponse{}, err } } if req.Expiration != nil { err = h.updateUserExpiration(ctx, tx, req.Username, req.Expiration) if err != nil { return dbplugin.UpdateUserResponse{}, err } } // Commit the transaction if err := tx.Commit(); err != nil { return dbplugin.UpdateUserResponse{}, err } return dbplugin.UpdateUserResponse{}, nil } func (h *HANA) updateUserPassword(ctx context.Context, tx *sql.Tx, username string, req *dbplugin.ChangePassword) error { password := req.NewPassword if username == "" || password == "" { return fmt.Errorf("must provide both username and password") } stmts := req.Statements.Commands if len(stmts) == 0 { stmts = []string{"ALTER USER {{username}} PASSWORD \"{{password}}\""} } for _, stmt := range stmts { for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") { query = strings.TrimSpace(query) if len(query) == 0 { continue } m := map[string]string{ "name": username, "username": username, "password": password, } if err := dbtxn.ExecuteTxQueryDirect(ctx, tx, m, query); err != nil { return fmt.Errorf("failed to execute query: %w", err) } } } return nil } func (h *HANA) updateUserExpiration(ctx context.Context, tx *sql.Tx, username string, req *dbplugin.ChangeExpiration) error { // If expiration is in the role SQL, HANA will deactivate the user when time is up, // regardless of whether vault is alive to revoke lease expirationStr := req.NewExpiration.String() if username == "" || expirationStr == "" { return fmt.Errorf("must provide both username and expiration") } stmts := req.Statements.Commands if len(stmts) == 0 { stmts = []string{"ALTER USER {{username}} VALID UNTIL '{{expiration}}'"} } for _, stmt := range stmts { for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") { query = strings.TrimSpace(query) if len(query) == 0 { continue } m := map[string]string{ "name": username, "username": username, "expiration": expirationStr, } if err := dbtxn.ExecuteTxQueryDirect(ctx, tx, m, query); err != nil { return fmt.Errorf("failed to execute query: %w", err) } } } return nil } // Revoking hana user will deactivate user and try to perform a soft drop func (h *HANA) DeleteUser(ctx context.Context, req dbplugin.DeleteUserRequest) (dbplugin.DeleteUserResponse, error) { h.Lock() defer h.Unlock() // default revoke will be a soft drop on user if len(req.Statements.Commands) == 0 { return h.revokeUserDefault(ctx, req) } // Get connection db, err := h.getConnection(ctx) if err != nil { return dbplugin.DeleteUserResponse{}, err } // Start a transaction tx, err := db.BeginTx(ctx, nil) if err != nil { return dbplugin.DeleteUserResponse{}, err } defer tx.Rollback() // Execute each query for _, stmt := range req.Statements.Commands { for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") { query = strings.TrimSpace(query) if len(query) == 0 { continue } m := map[string]string{ "name": req.Username, } if err := dbtxn.ExecuteTxQueryDirect(ctx, tx, m, query); err != nil { return dbplugin.DeleteUserResponse{}, err } } } return dbplugin.DeleteUserResponse{}, tx.Commit() } func (h *HANA) revokeUserDefault(ctx context.Context, req dbplugin.DeleteUserRequest) (dbplugin.DeleteUserResponse, error) { // Get connection db, err := h.getConnection(ctx) if err != nil { return dbplugin.DeleteUserResponse{}, err } // Start a transaction tx, err := db.BeginTx(ctx, nil) if err != nil { return dbplugin.DeleteUserResponse{}, err } defer tx.Rollback() // Disable server login for user disableStmt, err := tx.PrepareContext(ctx, fmt.Sprintf("ALTER USER %s DEACTIVATE USER NOW", req.Username)) if err != nil { return dbplugin.DeleteUserResponse{}, err } defer disableStmt.Close() if _, err := disableStmt.ExecContext(ctx); err != nil { return dbplugin.DeleteUserResponse{}, err } // Invalidates current sessions and performs soft drop (drop if no dependencies) // if hard drop is desired, custom revoke statements should be written for role dropStmt, err := tx.PrepareContext(ctx, fmt.Sprintf("DROP USER %s RESTRICT", req.Username)) if err != nil { return dbplugin.DeleteUserResponse{}, err } defer dropStmt.Close() if _, err := dropStmt.ExecContext(ctx); err != nil { return dbplugin.DeleteUserResponse{}, err } // Commit transaction if err := tx.Commit(); err != nil { return dbplugin.DeleteUserResponse{}, err } return dbplugin.DeleteUserResponse{}, nil }