diff --git a/changelog/16631.txt b/changelog/16631.txt new file mode 100644 index 0000000000000..73c46f65569a3 --- /dev/null +++ b/changelog/16631.txt @@ -0,0 +1,3 @@ +```release-note:feature +secrets/database/hana: Add ability to customize dynamic usernames +``` \ No newline at end of file diff --git a/plugins/database/hana/hana.go b/plugins/database/hana/hana.go index 51ac9784e268b..bca437c369a69 100644 --- a/plugins/database/hana/hana.go +++ b/plugins/database/hana/hana.go @@ -10,19 +10,22 @@ import ( "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/credsutil" "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" - maxIdentifierLength = 127 + 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) @@ -57,6 +60,25 @@ func (h *HANA) Initialize(ctx context.Context, req dbplugin.InitializeRequest) ( 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 @@ -94,13 +116,7 @@ func (h *HANA) NewUser(ctx context.Context, req dbplugin.NewUserRequest) (respon } // Generate username - username, err := credsutil.GenerateUsername( - credsutil.DisplayName(req.UsernameConfig.DisplayName, 32), - credsutil.RoleName(req.UsernameConfig.RoleName, 20), - credsutil.MaxLength(maxIdentifierLength), - credsutil.Separator("_"), - credsutil.ToUpper(), - ) + username, err := h.usernameProducer.Generate(req.UsernameConfig) if err != nil { return dbplugin.NewUserResponse{}, err } diff --git a/plugins/database/hana/hana_test.go b/plugins/database/hana/hana_test.go index 6a4de72c21943..67c1088834897 100644 --- a/plugins/database/hana/hana_test.go +++ b/plugins/database/hana/hana_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/vault/sdk/database/dbplugin/v5" dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing" + "github.com/stretchr/testify/require" ) func TestHANA_Initialize(t *testing.T) { @@ -288,6 +289,97 @@ func copyConfig(config map[string]interface{}) map[string]interface{} { return newConfig } +func TestHANA_DefaultUsernameTemplate(t *testing.T) { + if os.Getenv("HANA_URL") == "" || os.Getenv("VAULT_ACC") != "1" { + t.SkipNow() + } + connURL := os.Getenv("HANA_URL") + + connectionDetails := map[string]interface{}{ + "connection_url": connURL, + } + + initReq := dbplugin.InitializeRequest{ + Config: connectionDetails, + VerifyConnection: true, + } + + db := new() + dbtesting.AssertInitialize(t, db, initReq) + + usernameConfig := dbplugin.UsernameMetadata{ + DisplayName: "test", + RoleName: "test", + } + + const password = "SuperSecurePa55w0rd!" + resp := dbtesting.AssertNewUser(t, db, dbplugin.NewUserRequest{ + UsernameConfig: usernameConfig, + Password: password, + Statements: dbplugin.Statements{ + Commands: []string{testHANARole}, + }, + Expiration: time.Now().Add(5 * time.Minute), + }) + username := resp.Username + + if resp.Username == "" { + t.Fatalf("Missing username") + } + + testCredsExist(t, connURL, username, password) + + require.Regexp(t, `^V_TEST_TEST_[A-Z0-9]{20}_[0-9]{10}$`, resp.Username) + + defer dbtesting.AssertClose(t, db) +} + +func TestHANA_CustomUsernameTemplate(t *testing.T) { + if os.Getenv("HANA_URL") == "" || os.Getenv("VAULT_ACC") != "1" { + t.SkipNow() + } + connURL := os.Getenv("HANA_URL") + + connectionDetails := map[string]interface{}{ + "connection_url": connURL, + "username_template": "{{.DisplayName}}_{{random 10}}", + } + + initReq := dbplugin.InitializeRequest{ + Config: connectionDetails, + VerifyConnection: true, + } + + db := new() + dbtesting.AssertInitialize(t, db, initReq) + + usernameConfig := dbplugin.UsernameMetadata{ + DisplayName: "test", + RoleName: "test", + } + + const password = "SuperSecurePa55w0rd!" + resp := dbtesting.AssertNewUser(t, db, dbplugin.NewUserRequest{ + UsernameConfig: usernameConfig, + Password: password, + Statements: dbplugin.Statements{ + Commands: []string{testHANARole}, + }, + Expiration: time.Now().Add(5 * time.Minute), + }) + username := resp.Username + + if resp.Username == "" { + t.Fatalf("Missing username") + } + + testCredsExist(t, connURL, username, password) + + require.Regexp(t, `^TEST_[A-Z0-9]{10}$`, resp.Username) + + defer dbtesting.AssertClose(t, db) +} + const testHANARole = ` CREATE USER {{name}} PASSWORD "{{password}}" NO FORCE_FIRST_PASSWORD_CHANGE VALID UNTIL '{{expiration}}';` diff --git a/website/content/api-docs/secret/databases/hanadb.mdx b/website/content/api-docs/secret/databases/hanadb.mdx index d6764b92c9a8f..031a30a0aa3f1 100644 --- a/website/content/api-docs/secret/databases/hanadb.mdx +++ b/website/content/api-docs/secret/databases/hanadb.mdx @@ -44,6 +44,8 @@ has a number of parameters to further configure a connection. - `password` `(string: "")` - The root credential password used in the connection URL. +- `username_template` `(string)` - [Template](/docs/concepts/username-templating) describing how dynamic usernames are generated. + - `disable_escaping` `(boolean: false)` - Turns off the escaping of special characters inside of the username and password fields. See the [databases secrets engine docs](/docs/secrets/databases#disable-character-escaping) for more information. Defaults to `false`. diff --git a/website/content/docs/secrets/databases/index.mdx b/website/content/docs/secrets/databases/index.mdx index 2b2fbc2ab4d18..63eac385f03ba 100644 --- a/website/content/docs/secrets/databases/index.mdx +++ b/website/content/docs/secrets/databases/index.mdx @@ -138,7 +138,7 @@ and private key pair to authenticate. | [Cassandra](/docs/secrets/databases/cassandra) | Yes | Yes | Yes (1.6+) | Yes (1.7+) | password | | [Couchbase](/docs/secrets/databases/couchbase) | Yes | Yes | Yes | Yes (1.7+) | password | | [Elasticsearch](/docs/secrets/databases/elasticdb) | Yes | Yes | Yes (1.6+) | Yes (1.8+) | password | -| [HanaDB](/docs/secrets/databases/hanadb) | Yes (1.6+) | Yes | Yes (1.6+) | No | password | +| [HanaDB](/docs/secrets/databases/hanadb) | Yes (1.6+) | Yes | Yes (1.6+) | Yes (1.12+) | password | | [InfluxDB](/docs/secrets/databases/influxdb) | Yes | Yes | Yes (1.6+) | Yes (1.8+) | password | | [MongoDB](/docs/secrets/databases/mongodb) | Yes | Yes | Yes | Yes (1.7+) | password | | [MongoDB Atlas](/docs/secrets/databases/mongodbatlas) | No | Yes | Yes | Yes (1.8+) | password |