mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +00:00 
			
		
		
		
	Add the static-roles feature for MSSQL (#9062)
This commit is contained in:
		 Johnathan Schmidt
					Johnathan Schmidt
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						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