From 99208050c66cae3b29787bd9c0e137c857fed0f6 Mon Sep 17 00:00:00 2001 From: "johnathan.vieira@softplan.com.br" Date: Wed, 20 May 2020 21:41:02 -0300 Subject: [PATCH] Add the static-roles feature for MSSQL * Implement SetCredentials function with proper SQL statement * Implement prepareMSSQLTestContainer to create test container * Refactor existing tests to use the container - Inspired by the PostgreSQL and MySQL plugin implementations --- plugins/database/mssql/mssql.go | 69 ++++++- plugins/database/mssql/mssql_test.go | 168 ++++++++++++++++++ .../pages/docs/secrets/databases/index.mdx | 2 +- .../pages/docs/secrets/databases/mssql.mdx | 2 +- 4 files changed, 236 insertions(+), 5 deletions(-) diff --git a/plugins/database/mssql/mssql.go b/plugins/database/mssql/mssql.go index 9d1f39b395b6..52023fcf2b51 100644 --- a/plugins/database/mssql/mssql.go +++ b/plugins/database/mssql/mssql.go @@ -307,7 +307,7 @@ func (m *MSSQL) RotateRootCredentials(ctx context.Context, statements []string) rotateStatents := statements if len(rotateStatents) == 0 { - rotateStatents = []string{rotateRootCredentialsSQL} + rotateStatents = []string{alterLoginSQL} } db, err := m.getConnection(ctx) @@ -357,6 +357,70 @@ func (m *MSSQL) RotateRootCredentials(ctx context.Context, statements []string) return m.RawConfig, nil } +func (m *MSSQL) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticUser dbplugin.StaticUserConfig) (username, password string, err error) { + if len(statements.Rotation) == 0 { + statements.Rotation = []string{alterLoginSQL} + } + + username = staticUser.Username + password = staticUser.Password + + if username == "" || password == "" { + return "", "", errors.New("must provide both username and password") + } + + m.Lock() + defer m.Unlock() + + db, err := m.getConnection(ctx) + if err != nil { + return "", "", err + } + + var exists bool + + err = db.QueryRowContext(ctx, "SELECT 1 FROM master.sys.server_principals where name = N'$1'", username).Scan(&exists) + + if err != nil && err != sql.ErrNoRows { + return "", "", err + } + + stmts := statements.Rotation + + // Start a transaction + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return "", "", err + } + defer func() { + _ = tx.Rollback() + }() + + 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.ExecuteTxQuery(ctx, tx, m, query); err != nil { + return "", "", err + } + } + } + + if err := tx.Commit(); err != nil { + return "", "", err + } + + return username, password, nil +} + const dropUserSQL = ` USE [%s] IF EXISTS @@ -377,7 +441,6 @@ BEGIN DROP LOGIN [%s] END ` - -const rotateRootCredentialsSQL = ` +const alterLoginSQL = ` ALTER LOGIN [{{username}}] WITH PASSWORD = '{{password}}' ` diff --git a/plugins/database/mssql/mssql_test.go b/plugins/database/mssql/mssql_test.go index 94d18bb70920..e8522ac8b281 100644 --- a/plugins/database/mssql/mssql_test.go +++ b/plugins/database/mssql/mssql_test.go @@ -10,6 +10,7 @@ import ( mssqlhelper "github.com/hashicorp/vault/helper/testhelpers/mssql" "github.com/hashicorp/vault/sdk/database/dbplugin" + "github.com/hashicorp/vault/sdk/helper/dbtxn" ) func TestMSSQL_Initialize(t *testing.T) { @@ -123,6 +124,138 @@ func TestMSSQL_RotateRootCredentials(t *testing.T) { } } +func TestMSSQL_SetCredentials_missingArgs(t *testing.T) { + type testCase struct { + statements dbplugin.Statements + userConfig dbplugin.StaticUserConfig + } + + tests := map[string]testCase{ + "empty rotation statements": { + statements: dbplugin.Statements{ + Rotation: nil, + }, + userConfig: dbplugin.StaticUserConfig{ + Username: "testuser", + Password: "password", + }, + }, + "empty username": { + statements: dbplugin.Statements{ + Rotation: []string{` + ALTER LOGIN [{{username}}] WITH PASSWORD = '{{password}}';`, + }, + }, + userConfig: dbplugin.StaticUserConfig{ + Username: "", + Password: "password", + }, + }, + "empty password": { + statements: dbplugin.Statements{ + Rotation: []string{` + ALTER LOGIN [{{username}}] WITH PASSWORD = '{{password}}';`, + }, + }, + userConfig: dbplugin.StaticUserConfig{ + Username: "testuser", + Password: "", + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + db := new() + + username, password, err := db.SetCredentials(context.Background(), test.statements, test.userConfig) + if err == nil { + t.Fatalf("expected err, got nil") + } + if username != "" { + t.Fatalf("expected empty username, got [%s]", username) + } + if password != "" { + t.Fatalf("expected empty password, got [%s]", password) + } + }) + } +} + +func TestMSSQL_SetCredentials(t *testing.T) { + type testCase struct { + rotationStmts []string + } + + tests := map[string]testCase{ + "empty rotation statements": { + rotationStmts: []string{}, + }, "username rotation": { + rotationStmts: []string{` + ALTER LOGIN [{{username}}] WITH PASSWORD = '{{password}}';`, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + cleanup, connURL := mssqlhelper.PrepareMSSQLTestContainer(t) + defer cleanup() + + connectionDetails := map[string]interface{}{ + "connection_url": connURL, + } + + db := new() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + _, err := db.Init(ctx, connectionDetails, true) + if err != nil { + t.Fatalf("err: %s", err) + } + + dbUser := "vaultstatictest" + initPassword := "p4$sw0rd" + createTestMSSQLUser(t, connURL, dbUser, initPassword, testMSSQLLogin) + + if err := testCredsExist(t, connURL, dbUser, initPassword); err != nil { + t.Fatalf("Could not connect with initial credentials: %s", err) + } + + statements := dbplugin.Statements{ + Rotation: test.rotationStmts, + } + + newPassword, err := db.GenerateCredentials(context.Background()) + if err != nil { + t.Fatal(err) + } + + usernameConfig := dbplugin.StaticUserConfig{ + Username: dbUser, + Password: newPassword, + } + + username, password, err := db.SetCredentials(ctx, statements, usernameConfig) + if err != nil { + t.Fatalf("err: %s", err) + } + + if err := testCredsExist(t, connURL, username, password); err != nil { + t.Fatalf("Could not connect with new credentials: %s", err) + } + + if err := testCredsExist(t, connURL, username, initPassword); err == nil { + t.Fatalf("Should not be able to connect with initial credentials") + } + + }) + } + +} + func TestMSSQL_RevokeUser(t *testing.T) { cleanup, connURL := mssqlhelper.PrepareMSSQLTestContainer(t) defer cleanup() @@ -198,6 +331,37 @@ func testCredsExist(t testing.TB, connURL, username, password string) error { return db.Ping() } +func createTestMSSQLUser(t *testing.T, connURL string, username, password, query string) { + + db, err := sql.Open("mssql", connURL) + defer db.Close() + if err != nil { + t.Fatal(err) + } + + // Start a transaction + ctx := context.Background() + tx, err := db.BeginTx(ctx, nil) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = tx.Rollback() + }() + + m := map[string]string{ + "name": username, + "password": password, + } + if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil { + t.Fatal(err) + } + // Commit the transaction + if err := tx.Commit(); err != nil { + t.Fatal(err) + } +} + const testMSSQLRole = ` CREATE LOGIN [{{name}}] WITH PASSWORD = '{{password}}'; CREATE USER [{{name}}] FOR LOGIN [{{name}}]; @@ -207,3 +371,7 @@ const testMSSQLDrop = ` DROP USER [{{name}}]; DROP LOGIN [{{name}}]; ` + +const testMSSQLLogin = ` +CREATE LOGIN [{{name}}] WITH PASSWORD = '{{password}}'; +` diff --git a/website/pages/docs/secrets/databases/index.mdx b/website/pages/docs/secrets/databases/index.mdx index 2847da67df5a..f5295815a6e0 100644 --- a/website/pages/docs/secrets/databases/index.mdx +++ b/website/pages/docs/secrets/databases/index.mdx @@ -137,7 +137,7 @@ the proper permission, it can generate credentials. | [InfluxDB](/docs/secrets/databases/influxdb) | Yes | Yes | No | | [MongoDB](/docs/secrets/databases/mongodb) | No | Yes | Yes | | [MongoDB Atlas](/docs/secrets/databases/mongodbatlas) | No | Yes | Yes | -| [MSSQL](/docs/secrets/databases/mssql) | Yes | Yes | No | +| [MSSQL](/docs/secrets/databases/mssql) | Yes | Yes | Yes | | [MySQL/MariaDB](/docs/secrets/databases/mysql-maria) | Yes | Yes | Yes | | [Oracle](/docs/secrets/databases/oracle) | Yes | Yes | Yes | | [PostgreSQL](/docs/secrets/databases/postgresql) | Yes | Yes | Yes | diff --git a/website/pages/docs/secrets/databases/mssql.mdx b/website/pages/docs/secrets/databases/mssql.mdx index 5e775fa00a97..671652b7b490 100644 --- a/website/pages/docs/secrets/databases/mssql.mdx +++ b/website/pages/docs/secrets/databases/mssql.mdx @@ -22,7 +22,7 @@ more information about setting up the database secrets engine. | Plugin Name | Root Credential Rotation | Dynamic Roles | Static Roles | | ----------------------- | ------------------------ | ------------- | ------------ | -| `mssql-database-plugin` | Yes | Yes | No | +| `mssql-database-plugin` | Yes | Yes | Yes | ## Setup