Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added HANA database plugin #2811

Merged
merged 15 commits into from
Jul 7, 2017
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions helper/builtinplugins/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package builtinplugins

import (
"github.com/hashicorp/vault/plugins/database/cassandra"
"github.com/hashicorp/vault/plugins/database/hana"
"github.com/hashicorp/vault/plugins/database/mongodb"
"github.com/hashicorp/vault/plugins/database/mssql"
"github.com/hashicorp/vault/plugins/database/mysql"
Expand All @@ -22,6 +23,7 @@ var plugins map[string]BuiltinFactory = map[string]BuiltinFactory{
"mssql-database-plugin": mssql.New,
"cassandra-database-plugin": cassandra.New,
"mongodb-database-plugin": mongodb.New,
"hana-database-plugin": hana.New,
}

func Get(name string) (BuiltinFactory, bool) {
Expand Down
21 changes: 21 additions & 0 deletions plugins/database/hana/hana-database-plugin/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

import (
"log"
"os"

"github.com/hashicorp/vault/helper/pluginutil"
"github.com/hashicorp/vault/plugins/database/hana"
)

func main() {
apiClientMeta := &pluginutil.APIClientMeta{}
flags := apiClientMeta.FlagSet()
flags.Parse(os.Args)

err := hana.Run(apiClientMeta.GetTLSConfig())
if err != nil {
log.Println(err)
os.Exit(1)
}
}
282 changes: 282 additions & 0 deletions plugins/database/hana/hana.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
package hana

import (
"database/sql"
"fmt"
"strings"
"time"

"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
"github.com/hashicorp/vault/helper/strutil"
"github.com/hashicorp/vault/plugins"
"github.com/hashicorp/vault/plugins/helper/database/connutil"
"github.com/hashicorp/vault/plugins/helper/database/credsutil"
"github.com/hashicorp/vault/plugins/helper/database/dbutil"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that here you will need to import _ "github.com/SAP/go-hdb/driver"

)

const (
hanaTypeName = "hdb"
)

// HANA is an implementation of Database interface
type HANA struct {
connutil.ConnectionProducer
credsutil.CredentialsProducer
}

// New implements builtinplugins.BuiltinFactory
func New() (interface{}, error) {
connProducer := &connutil.SQLConnectionProducer{}
connProducer.Type = hanaTypeName

credsProducer := &credsutil.SQLCredentialsProducer{
DisplayNameLen: 32,
RoleNameLen: 20,
UsernameLen: 128,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RolenameLen and Separator should be specified here as well!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will change. Seems like that functionality was added only recently

Separator: "_",
}

dbType := &HANA{
ConnectionProducer: connProducer,
CredentialsProducer: credsProducer,
}

return dbType, nil
}

// Run instantiates a HANA object, and runs the RPC server for the plugin
func Run(apiTLSConfig *api.TLSConfig) error {
dbType, err := New()
if err != nil {
return err
}

plugins.Serve(dbType.(*HANA), apiTLSConfig)

return nil
}

// Type returns the TypeName for this backend
func (h *HANA) Type() (string, error) {
return hanaTypeName, nil
}

func (h *HANA) getConnection() (*sql.DB, error) {
db, err := h.Connection()
if err != nil {
return nil, err
}

return db.(*sql.DB), nil
}

// CreateUser generates the username/password on the underlying HANA secret backend
// as instructed by the CreationStatement provided.
func (h *HANA) CreateUser(statements dbplugin.Statements, usernameConfig dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) {
// Grab the lock
h.Lock()
defer h.Unlock()

// Get the connection
db, err := h.getConnection()
if err != nil {
return "", "", err
}

if statements.CreationStatements == "" {
return "", "", dbutil.ErrEmptyCreationStatement
}

// Generate username
username, err = h.GenerateUsername(usernameConfig)
if err != nil {
return "", "", err
}

// HANA does not allow hyphens in usernames, and highly prefers capital letters
username = strings.Replace(username, "-", "_", -1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be done by setting the Separator variable in the Credential producer object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This in fact, cannot be done simply by setting the Separator variable.
If the displayname or rolename contains -, the username will be invalid for HANA db.
Therefore, even though Separator = "_", there is still an explicit conversion here.

username = strings.ToUpper(username)

// Generate password
password, err = h.GeneratePassword()
if err != nil {
return "", "", err
}
// Most HANA configurations have password constraints
// Prefix with A1a to satisfy these constraints. User will be forced to change upon login
password = strings.Replace(password, "-", "_", -1)
password = "A1a" + password
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does the "A1a" string do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most HANA dbs have a strict password policy. That is, they require one upper case, one numeric, and one lower case character (most widely used policy).

This prefix ensures that the newly generated password will conform to the policy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Password is prefixed with A1a to satisfy the most common HANA db password requirement (one upper case, one lower case, one numeric)


// 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, err := h.GenerateExpiration(expiration)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expiration string generation has been changed to the default way

if err != nil {
return "", "", err
}

// Start a transaction
tx, err := db.Begin()
if err != nil {
return "", "", err
}
defer tx.Rollback()

// Execute each query
for _, query := range strutil.ParseArbitraryStringSlice(statements.CreationStatements, ";") {
query = strings.TrimSpace(query)
if len(query) == 0 {
continue
}

stmt, err := tx.Prepare(dbutil.QueryHelper(query, map[string]string{
"name": username,
"password": password,
"expiration": expirationStr,
}))
if err != nil {
return "", "", err
}
defer stmt.Close()
if _, err := stmt.Exec(); err != nil {
return "", "", err
}
}

// Commit the transaction
if err := tx.Commit(); err != nil {
return "", "", err
}

return username, password, nil
}

// Renewing hana user just means altering user's valid until property
func (h *HANA) RenewUser(statements dbplugin.Statements, username string, expiration time.Time) error {
// Get connection
db, err := h.getConnection()
if err != nil {
return err
}

// Start a transaction
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()

// 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, err := h.GenerateExpiration(expiration)
if err != nil {
return err
}

// Renew user's valid until property field
stmt, err := tx.Prepare("ALTER USER " + username + " VALID UNTIL " + "'" + expirationStr + "'")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know a lot about HanaDB, would there be a usecase for a custom renew statement?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are asking the use case for this particular SQL:

If expiration field is in the role's SQL, the generated credential will be set to expire when the lease expires on HANA server side. This means, in order to renew a credential, the expiration date that HANA keeps for that user must also be pushed back. Hence this ALTER USER sql.

If you are asking for the use case of potential custom renew statements that users may want to input:

I am unaware of any workflows where this would be needed. But if the convention is to make custom renew statements available, I can do that too.

if err != nil {
return err
}
defer stmt.Close()
if _, err := stmt.Exec(); err != nil {
return err
}

// Commit the transaction
if err := tx.Commit(); err != nil {
return err
}

return nil
}

// Revoking hana user will deactivate user and try to perform a soft drop
func (h *HANA) RevokeUser(statements dbplugin.Statements, username string) error {
// default revoke will be a soft drop on user
if statements.RevocationStatements == "" {
return h.revokeUserDefault(username)
}

// Get connection
db, err := h.getConnection()
if err != nil {
return err
}

// Start a transaction
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()

// Execute each query
for _, query := range strutil.ParseArbitraryStringSlice(statements.RevocationStatements, ";") {
query = strings.TrimSpace(query)
if len(query) == 0 {
continue
}

stmt, err := tx.Prepare(dbutil.QueryHelper(query, map[string]string{
"name": username,
}))
if err != nil {
return err
}
defer stmt.Close()
if _, err := stmt.Exec(); err != nil {
return err
}
}

// Commit the transaction
if err := tx.Commit(); err != nil {
return err
}

return nil
}

func (h *HANA) revokeUserDefault(username string) error {
// Get connection
db, err := h.getConnection()
if err != nil {
return err
}

// Start a transaction
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()

// Disable server login for user
disableStmt, err := tx.Prepare(fmt.Sprintf("ALTER USER %s DEACTIVATE USER NOW", username))
if err != nil {
return err
}
defer disableStmt.Close()
if _, err := disableStmt.Exec(); err != nil {
return 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.Prepare(fmt.Sprintf("DROP USER %s RESTRICT", username))
if err != nil {
return err
}
defer dropStmt.Close()
if _, err := dropStmt.Exec(); err != nil {
return err
}

// Commit transaction
if err := tx.Commit(); err != nil {
return err
}

return nil
}
Loading