mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-11-01 02:57:59 +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 | ||||
| 	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}}'  | ||||
| ` | ||||
|   | ||||
| @@ -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}}'; | ||||
| ` | ||||
|   | ||||
| @@ -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 | | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user