From 04e75372fb30a4ec03645ca1de60bba54c2bc9dc Mon Sep 17 00:00:00 2001 From: John-Michael Faircloth Date: Fri, 24 Jan 2025 14:42:27 -0600 Subject: [PATCH] database/mssql: set default root rotation stmt for contained db (#29399) * database/mssql: set default root rotation stmt for contained db * changelog * add rotate root test * fix test * update passwords to make mssql happy * create admin user * update contained user create query * remove test --- changelog/29399.txt | 3 +++ plugins/database/mssql/mssql.go | 14 +++++++++++- plugins/database/mssql/mssql_test.go | 22 +++++++++++++------ .../content/docs/secrets/databases/mssql.mdx | 15 +++++++++++-- 4 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 changelog/29399.txt diff --git a/changelog/29399.txt b/changelog/29399.txt new file mode 100644 index 000000000000..1969a973a9cf --- /dev/null +++ b/changelog/29399.txt @@ -0,0 +1,3 @@ +```release-note:bug +database/mssql: Fix a bug where contained databases would silently fail root rotation if a custom root rotation statement was not provided. +``` diff --git a/plugins/database/mssql/mssql.go b/plugins/database/mssql/mssql.go index 488d7f39226d..fdeaeff86c6d 100644 --- a/plugins/database/mssql/mssql.go +++ b/plugins/database/mssql/mssql.go @@ -345,8 +345,11 @@ func (m *MSSQL) UpdateUser(ctx context.Context, req dbplugin.UpdateUserRequest) func (m *MSSQL) updateUserPass(ctx context.Context, username string, changePass *dbplugin.ChangePassword) error { stmts := changePass.Statements.Commands - if len(stmts) == 0 && !m.containedDB { + if len(stmts) == 0 { stmts = []string{alterLoginSQL} + if m.containedDB { + stmts = []string{alterUserContainedSQL} + } } password := changePass.NewPassword @@ -384,6 +387,11 @@ func (m *MSSQL) updateUserPass(ctx context.Context, username string, changePass _ = tx.Rollback() }() + if len(stmts) == 0 { + // should not happen, but guard against it anyway + return errors.New("no statement provided") + } + for _, stmt := range stmts { for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") { query = strings.TrimSpace(query) @@ -431,3 +439,7 @@ EXEC (@stmt)` const alterLoginSQL = ` ALTER LOGIN [{{username}}] WITH PASSWORD = '{{password}}' ` + +const alterUserContainedSQL = ` +ALTER USER [{{username}}] WITH PASSWORD = '{{password}}' +` diff --git a/plugins/database/mssql/mssql_test.go b/plugins/database/mssql/mssql_test.go index d549d52cd178..2a32f610c96e 100644 --- a/plugins/database/mssql/mssql_test.go +++ b/plugins/database/mssql/mssql_test.go @@ -20,7 +20,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestInitialize(t *testing.T) { +func TestMSSQLInitialize(t *testing.T) { cleanup, connURL := mssqlhelper.PrepareMSSQLTestContainer(t) defer cleanup() @@ -79,7 +79,7 @@ func TestInitialize(t *testing.T) { } } -func TestNewUser(t *testing.T) { +func TestMSSQLNewUser(t *testing.T) { cleanup, connURL := mssqlhelper.PrepareMSSQLTestContainer(t) defer cleanup() @@ -185,7 +185,7 @@ func TestNewUser(t *testing.T) { } } -func TestUpdateUser_password(t *testing.T) { +func TestMSSQLUpdateUser_password(t *testing.T) { type testCase struct { req dbplugin.UpdateUserRequest expectErr bool @@ -312,7 +312,7 @@ func TestUpdateUser_password(t *testing.T) { } } -func TestDeleteUser(t *testing.T) { +func TestMSSQLDeleteUser(t *testing.T) { cleanup, connURL := mssqlhelper.PrepareMSSQLTestContainer(t) defer cleanup() @@ -358,7 +358,7 @@ func TestDeleteUser(t *testing.T) { assertCredsDoNotExist(t, connURL, dbUser, initPassword) } -func TestDeleteUserContainedDB(t *testing.T) { +func TestMSSQLDeleteUserContainedDB(t *testing.T) { cleanup, connURL := mssqlhelper.PrepareMSSQLTestContainer(t) defer cleanup() @@ -405,7 +405,7 @@ func TestDeleteUserContainedDB(t *testing.T) { assertContainedDBCredsDoNotExist(t, connURL, dbUser) } -func TestContainedDBSQLSanitization(t *testing.T) { +func TestMSSQLContainedDBSQLSanitization(t *testing.T) { cleanup, connURL := mssqlhelper.PrepareMSSQLTestContainer(t) defer cleanup() @@ -443,7 +443,7 @@ func TestContainedDBSQLSanitization(t *testing.T) { assert.EqualError(t, err, "mssql: Cannot alter the login 'vaultuser]', because it does not exist or you do not have permission.") } -func TestSQLSanitization(t *testing.T) { +func TestMSSQLSanitization(t *testing.T) { cleanup, connURL := mssqlhelper.PrepareMSSQLTestContainer(t) defer cleanup() @@ -576,3 +576,11 @@ const testMSSQLContainedLogin = ` CREATE LOGIN [{{name}}] WITH PASSWORD = '{{password}}'; CREATE USER [{{name}}] FOR LOGIN [{{name}}]; ` + +const testMSSQLContainedLoginAdmin = ` +CREATE USER [{{name}}] WITH PASSWORD = '{{password}}'; + +ALTER ROLE db_datareader ADD MEMBER [{{name}}]; +ALTER ROLE db_datawriter ADD MEMBER [{{name}}]; +ALTER ROLE db_owner ADD MEMBER [{{name}}]; +` diff --git a/website/content/docs/secrets/databases/mssql.mdx b/website/content/docs/secrets/databases/mssql.mdx index 57d46e1a47b9..96b9f4576260 100644 --- a/website/content/docs/secrets/databases/mssql.mdx +++ b/website/content/docs/secrets/databases/mssql.mdx @@ -115,13 +115,23 @@ the proper permission, it can generate credentials. ## Example for Azure SQL database -Here is a complete example using Azure SQL Database. Note that databases in Azure SQL Database are [contained databases](https://docs.microsoft.com/en-us/sql/relational-databases/databases/contained-databases) and that we do not create a login for the user; instead, we associate the password directly with the user itself. Also note that you will need a separate connection and role for each Azure SQL database for which you want to generate dynamic credentials. You can use a single database backend mount for all these databases or use a separate mount for each of them. In this example, we use a custom path for the database backend. +Here is a complete example using Azure SQL Database. Note that databases in +Azure SQL Database are [contained databases](https://docs.microsoft.com/en-us/sql/relational-databases/databases/contained-databases) +and that we do not create a login for the user; instead, we associate the +password directly with the user itself. Also note that you will need a separate +connection and role for each Azure SQL database for which you want to generate +dynamic credentials. You can use a single database backend mount for all these +databases or use a separate mount for each of them. In this example, we use a +custom path for the database backend. Azure SQL databases may use different authentication mechanism that are configured on the SQL server. Vault only supports SQL authentication. Azure AD authentication is not supported. -First, we mount a database backend at the azuresql path with `vault secrets enable -path=azuresql database`. Then we configure a connection called "testvault" to connect to a database called "test-vault", using "azuresql" at the beginning of our path: +First, we mount a database backend at the azuresql path with `vault secrets +enable -path=azuresql database`. Then we configure a connection called +"testvault" to connect to a database called "test-vault", using "azuresql" at +the beginning of our path and set the `contained_db` field: ~> Note: If you are using a windows vault client with cmd.exe, change the single quotes to double quotes in the connection string. Windows cmd.exe does not interpret single quotes as a continous string @@ -129,6 +139,7 @@ First, we mount a database backend at the azuresql path with `vault secrets enab $ vault write azuresql/config/testvault \ plugin_name=mssql-database-plugin \ connection_url='server=hashisqlserver.database.windows.net;port=1433;user id=admin;password=pAssw0rd;database=test-vault;app name=vault;' \ + contained_db=true \ allowed_roles="test" ```