mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-11-03 20:17:59 +00:00 
			
		
		
		
	Add the static-roles feature for MSSQL (#9062)
This commit is contained in:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							df5d330ead
						
					
				
				
					commit
					054eec2671
				
			@@ -307,7 +307,7 @@ func (m *MSSQL) RotateRootCredentials(ctx context.Context, statements []string)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	rotateStatents := statements
 | 
						rotateStatents := statements
 | 
				
			||||||
	if len(rotateStatents) == 0 {
 | 
						if len(rotateStatents) == 0 {
 | 
				
			||||||
		rotateStatents = []string{rotateRootCredentialsSQL}
 | 
							rotateStatents = []string{alterLoginSQL}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	db, err := m.getConnection(ctx)
 | 
						db, err := m.getConnection(ctx)
 | 
				
			||||||
@@ -357,6 +357,70 @@ func (m *MSSQL) RotateRootCredentials(ctx context.Context, statements []string)
 | 
				
			|||||||
	return m.RawConfig, nil
 | 
						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 = `
 | 
					const dropUserSQL = `
 | 
				
			||||||
USE [%s]
 | 
					USE [%s]
 | 
				
			||||||
IF EXISTS
 | 
					IF EXISTS
 | 
				
			||||||
@@ -377,7 +441,6 @@ BEGIN
 | 
				
			|||||||
  DROP LOGIN [%s]
 | 
					  DROP LOGIN [%s]
 | 
				
			||||||
END
 | 
					END
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					const alterLoginSQL = `
 | 
				
			||||||
const rotateRootCredentialsSQL = `
 | 
					 | 
				
			||||||
ALTER LOGIN [{{username}}] WITH PASSWORD = '{{password}}' 
 | 
					ALTER LOGIN [{{username}}] WITH PASSWORD = '{{password}}' 
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	mssqlhelper "github.com/hashicorp/vault/helper/testhelpers/mssql"
 | 
						mssqlhelper "github.com/hashicorp/vault/helper/testhelpers/mssql"
 | 
				
			||||||
	"github.com/hashicorp/vault/sdk/database/dbplugin"
 | 
						"github.com/hashicorp/vault/sdk/database/dbplugin"
 | 
				
			||||||
 | 
						"github.com/hashicorp/vault/sdk/helper/dbtxn"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestMSSQL_Initialize(t *testing.T) {
 | 
					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) {
 | 
					func TestMSSQL_RevokeUser(t *testing.T) {
 | 
				
			||||||
	cleanup, connURL := mssqlhelper.PrepareMSSQLTestContainer(t)
 | 
						cleanup, connURL := mssqlhelper.PrepareMSSQLTestContainer(t)
 | 
				
			||||||
	defer cleanup()
 | 
						defer cleanup()
 | 
				
			||||||
@@ -198,6 +331,37 @@ func testCredsExist(t testing.TB, connURL, username, password string) error {
 | 
				
			|||||||
	return db.Ping()
 | 
						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 = `
 | 
					const testMSSQLRole = `
 | 
				
			||||||
CREATE LOGIN [{{name}}] WITH PASSWORD = '{{password}}';
 | 
					CREATE LOGIN [{{name}}] WITH PASSWORD = '{{password}}';
 | 
				
			||||||
CREATE USER [{{name}}] FOR LOGIN [{{name}}];
 | 
					CREATE USER [{{name}}] FOR LOGIN [{{name}}];
 | 
				
			||||||
@@ -207,3 +371,7 @@ const testMSSQLDrop = `
 | 
				
			|||||||
DROP USER [{{name}}];
 | 
					DROP USER [{{name}}];
 | 
				
			||||||
DROP LOGIN [{{name}}];
 | 
					DROP LOGIN [{{name}}];
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const testMSSQLLogin = `
 | 
				
			||||||
 | 
					CREATE LOGIN [{{name}}] WITH PASSWORD = '{{password}}';
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -137,7 +137,7 @@ the proper permission, it can generate credentials.
 | 
				
			|||||||
| [InfluxDB](/docs/secrets/databases/influxdb)          | Yes | Yes | No  |
 | 
					| [InfluxDB](/docs/secrets/databases/influxdb)          | Yes | Yes | No  |
 | 
				
			||||||
| [MongoDB](/docs/secrets/databases/mongodb)            | No  | Yes | Yes |
 | 
					| [MongoDB](/docs/secrets/databases/mongodb)            | No  | Yes | Yes |
 | 
				
			||||||
| [MongoDB Atlas](/docs/secrets/databases/mongodbatlas) | 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 |
 | 
					| [MySQL/MariaDB](/docs/secrets/databases/mysql-maria)  | Yes | Yes | Yes |
 | 
				
			||||||
| [Oracle](/docs/secrets/databases/oracle)              | Yes | Yes | Yes |
 | 
					| [Oracle](/docs/secrets/databases/oracle)              | Yes | Yes | Yes |
 | 
				
			||||||
| [PostgreSQL](/docs/secrets/databases/postgresql)      | Yes | Yes | Yes |
 | 
					| [PostgreSQL](/docs/secrets/databases/postgresql)      | Yes | Yes | Yes |
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,7 @@ more information about setting up the database secrets engine.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
| Plugin Name             | Root Credential Rotation | Dynamic Roles | Static Roles |
 | 
					| Plugin Name             | Root Credential Rotation | Dynamic Roles | Static Roles |
 | 
				
			||||||
| ----------------------- | ------------------------ | ------------- | ------------ |
 | 
					| ----------------------- | ------------------------ | ------------- | ------------ |
 | 
				
			||||||
| `mssql-database-plugin` | Yes                      | Yes           | No           |
 | 
					| `mssql-database-plugin` | Yes                      | Yes           | Yes          |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Setup
 | 
					## Setup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user