diff --git a/CHANGELOG.md b/CHANGELOG.md index 4954c44452..b0858125e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated Pester test guidance in AI instructions in community style guidelines. - Added SChannelDsc as a required module for integration tests and enabled the prerequisites tests `Ensure TLS 1.2 is enabled` ([issue #2441](https://github.com/dsccommunity/SqlServerDsc/issues/2441)). +- `SqlLogin` + - Added parameter `Language` to allow setting the default language used by the login. ## [17.5.0] - 2026-01-30 diff --git a/source/DSCResources/DSC_SqlLogin/DSC_SqlLogin.psm1 b/source/DSCResources/DSC_SqlLogin/DSC_SqlLogin.psm1 index ca450ab49d..a691294c09 100644 --- a/source/DSCResources/DSC_SqlLogin/DSC_SqlLogin.psm1 +++ b/source/DSCResources/DSC_SqlLogin/DSC_SqlLogin.psm1 @@ -71,6 +71,7 @@ function Get-TargetResource InstanceName = $InstanceName Disabled = $login.IsDisabled DefaultDatabase = $login.DefaultDatabase + Language = $login.Language } if ($login.LoginType -eq 'SqlLogin') @@ -120,6 +121,9 @@ function Get-TargetResource .PARAMETER DefaultDatabase Specifies the default database for the login. + + .PARAMETER Language + Specifies the default language for the login. #> function Set-TargetResource { @@ -179,7 +183,11 @@ function Set-TargetResource [Parameter()] [System.String] - $DefaultDatabase + $DefaultDatabase, + + [Parameter()] + [System.String] + $Language ) $serverObject = Connect-SQL -ServerName $ServerName -InstanceName $InstanceName -ErrorAction 'Stop' @@ -263,9 +271,18 @@ function Set-TargetResource } } - if ( $PSBoundParameters.ContainsKey('DefaultDatabase') -and ($login.DefaultDatabase -ne $DefaultDatabase) ) + if ( ( $PSBoundParameters.ContainsKey('DefaultDatabase') -and ($login.DefaultDatabase -ne $DefaultDatabase) ) -or + ( $PSBoundParameters.ContainsKey('Language') -and $login.Language -ne $Language ) ) { - $login.DefaultDatabase = $DefaultDatabase + if ( $PSBoundParameters.ContainsKey('DefaultDatabase') ) + { + $login.DefaultDatabase = $DefaultDatabase + } + + if ( $PSBoundParameters.ContainsKey('Language') ) + { + $login.Language = $Language + } Update-SQLServerLogin -Login $login } } @@ -338,10 +355,20 @@ function Set-TargetResource $login.Disable() } - # Set the default database if specified - if ( $PSBoundParameters.ContainsKey('DefaultDatabase') ) + if ( ( $PSBoundParameters.ContainsKey('DefaultDatabase') -and ($login.DefaultDatabase -ne $DefaultDatabase) ) -or + ( $PSBoundParameters.ContainsKey('Language') -and $login.Language -ne $Language ) ) { - $login.DefaultDatabase = $DefaultDatabase + # Set the default database if specified + if ( $PSBoundParameters.ContainsKey('DefaultDatabase') ) + { + $login.DefaultDatabase = $DefaultDatabase + } + + # Set the language if specified + if ( $PSBoundParameters.ContainsKey('Language') ) + { + $login.Language = $Language + } Update-SQLServerLogin -Login $login } } @@ -398,6 +425,9 @@ function Set-TargetResource .PARAMETER DefaultDatabase Specifies the default database for the login. + + .PARAMETER Language + Specifies the default language for the login. #> function Test-TargetResource { @@ -457,7 +487,11 @@ function Test-TargetResource [Parameter()] [System.String] - $DefaultDatabase + $DefaultDatabase, + + [Parameter()] + [System.String] + $Language ) Write-Verbose -Message ( @@ -522,6 +556,15 @@ function Test-TargetResource $testPassed = $false } + if ( $PSBoundParameters.ContainsKey('Language') -and ($loginInfo.Language -ne $Language) ) + { + Write-Verbose -Message ( + $script:localizedData.WrongLanguage -f $Name, $loginInfo.Language, $Language + ) + + $testPassed = $false + } + if ( $LoginType -eq 'SqlLogin' ) { if ( $PSBoundParameters.ContainsKey('LoginPasswordExpirationEnabled') -and $LoginPasswordExpirationEnabled -ne $loginInfo.LoginPasswordExpirationEnabled ) diff --git a/source/DSCResources/DSC_SqlLogin/DSC_SqlLogin.schema.mof b/source/DSCResources/DSC_SqlLogin/DSC_SqlLogin.schema.mof index 8f3c01e23e..5ef638f33d 100644 --- a/source/DSCResources/DSC_SqlLogin/DSC_SqlLogin.schema.mof +++ b/source/DSCResources/DSC_SqlLogin/DSC_SqlLogin.schema.mof @@ -14,4 +14,5 @@ class DSC_SqlLogin : OMI_BaseResource [Write, Description("Specifies if the login password is required to conform to the password policy specified in the system security policy. Only applies to _SQL Logins_.")] Boolean LoginPasswordPolicyEnforced; [Write, Description("Specifies if the login is disabled. Default value is `$false`.")] Boolean Disabled; [Write, Description("Specifies the default database name.")] String DefaultDatabase; + [Write, Description("Specifies the default language.")] String Language; }; diff --git a/source/DSCResources/DSC_SqlLogin/en-US/DSC_SqlLogin.strings.psd1 b/source/DSCResources/DSC_SqlLogin/en-US/DSC_SqlLogin.strings.psd1 index 1d1292ed73..f76b737e10 100644 --- a/source/DSCResources/DSC_SqlLogin/en-US/DSC_SqlLogin.strings.psd1 +++ b/source/DSCResources/DSC_SqlLogin/en-US/DSC_SqlLogin.strings.psd1 @@ -19,6 +19,7 @@ ConvertFrom-StringData @' ExpectedDisabled = Expected the login '{0}' to be disabled, but it is enabled. ExpectedEnabled = Expected the login '{0}' to be enabled, but it is disabled. WrongDefaultDatabase = The login '{0}' has the default database '{1}', but expected it to have the default database '{2}'. + WrongLanguage = The login '{0}' has the default language '{1}', but expected it to have the default language '{2}'. ExpectedLoginPasswordExpirationDisabled = The login '{0}' has the password expiration enabled, but expected it to be disabled. ExpectedLoginPasswordExpirationEnabled = The login '{0}' has the password expiration disabled, but expected it to be enabled. ExpectedLoginPasswordPolicyEnforcedDisabled = The login '{0}' has the password policy enforced enabled, but expected it to be disabled. diff --git a/tests/Unit/DSC_SqlLogin.Tests.ps1 b/tests/Unit/DSC_SqlLogin.Tests.ps1 index 18ab29baaf..08fff817c9 100644 --- a/tests/Unit/DSC_SqlLogin.Tests.ps1 +++ b/tests/Unit/DSC_SqlLogin.Tests.ps1 @@ -116,6 +116,7 @@ Describe 'SqlLogin\Get-TargetResource' -Tag 'Get' { Add-Member -MemberType NoteProperty -Name 'Name' -Value $MockLoginName -PassThru | Add-Member -MemberType NoteProperty -Name 'LoginType' -Value $MockLoginType -PassThru | Add-Member -MemberType NoteProperty -Name 'DefaultDatabase' -Value 'master' -PassThru | + Add-Member -MemberType NoteProperty -Name 'Language' -Value 'us_english' -PassThru | Add-Member -MemberType NoteProperty -Name 'IsDisabled' -Value $true -PassThru | Add-Member -MemberType NoteProperty -Name 'MustChangePassword' -Value $true -PassThru | Add-Member -MemberType NoteProperty -Name 'PasswordExpirationEnabled' -Value $true -PassThru | @@ -145,6 +146,7 @@ Describe 'SqlLogin\Get-TargetResource' -Tag 'Get' { $result.LoginType | Should -Be $MockLoginType $result.Disabled | Should -BeTrue $result.DefaultDatabase | Should -Be 'master' + $result.Language | Should -Be 'us_english' if ($MockLoginType -eq 'SqlLogin') { @@ -197,6 +199,7 @@ Describe 'SqlLogin\Get-TargetResource' -Tag 'Get' { Add-Member -MemberType NoteProperty -Name 'Name' -Value 'Login1' -PassThru | Add-Member -MemberType NoteProperty -Name 'LoginType' -Value 'SqlLogin' -PassThru | Add-Member -MemberType NoteProperty -Name 'DefaultDatabase' -Value 'master' -PassThru | + Add-Member -MemberType NoteProperty -Name 'Language' -Value 'us_english' -PassThru | Add-Member -MemberType NoteProperty -Name 'IsDisabled' -Value $true -PassThru | Add-Member -MemberType NoteProperty -Name 'MustChangePassword' -Value $true -PassThru | Add-Member -MemberType NoteProperty -Name 'PasswordExpirationEnabled' -Value $true -PassThru | @@ -226,6 +229,7 @@ Describe 'SqlLogin\Get-TargetResource' -Tag 'Get' { $result.LoginType | Should -BeNullOrEmpty $result.Disabled | Should -BeFalse $result.DefaultDatabase | Should -BeNullOrEmpty + $result.Language | Should -BeNullOrEmpty if ($MockLoginType -eq 'SqlLogin') { @@ -456,12 +460,17 @@ Describe 'SqlLogin\Test-TargetResource' -Tag 'Test' { MockPropertyName = 'DefaultDatabase' MockPropertyValue = 'database1' } + @{ + MockPropertyName = 'Language' + MockPropertyValue = 'Français' + } ) { BeforeAll { Mock -CommandName Get-TargetResource -MockWith { return @{ Ensure = 'Present' DefaultDatabase = 'master' + Language = 'us_english' <# Switch the value of the property to the opposite of what will be specified in the call to Test-TargetResource. @@ -756,6 +765,42 @@ Describe 'SqlLogin\Set-TargetResource' -Tag 'Set' { } } + Context 'When creating a new login of type WindowsUser and specifying a language' { + BeforeAll { + $mockConnectSQL = { + return New-Object -TypeName Object | + Add-Member -MemberType 'NoteProperty' -Name 'LoginMode' -Value 'Integrated' -PassThru | + Add-Member -MemberType 'ScriptProperty' -Name 'Logins' -Value { + # Mocks no existing logins. + return @{} + } -PassThru -Force + } + + Mock -CommandName Connect-SQL -MockWith $mockConnectSQL + Mock -CommandName New-SQLServerLogin + Mock -CommandName Update-SQLServerLogin + } + + It 'Should not throw and call the correct mocks' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $mockSetTargetResourceParameters.Name = 'Windows\Login1' + $mockSetTargetResourceParameters.LoginType = 'WindowsUser' + $mockSetTargetResourceParameters.Language = 'Français' + + $null = Set-TargetResource @mockSetTargetResourceParameters -ErrorAction 'Stop' + } + + Should -Invoke -CommandName Connect-SQL -Exactly -Times 1 -Scope It + Should -Invoke -CommandName New-SQLServerLogin -ParameterFilter { + $Login.Name -eq 'Windows\Login1' } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Update-SQLServerLogin -ParameterFilter { + $Login.Name -eq 'Windows\Login1' -and $Login.Language -eq 'Français' + } -Exactly -Times 1 -Scope It + } + } + Context 'When creating a new login of type WindowsUser and specifying that it should be disabled' { BeforeAll { $mockConnectSQL = { @@ -941,6 +986,42 @@ Describe 'SqlLogin\Set-TargetResource' -Tag 'Set' { } } + Context 'When creating a new login of type SqlLogin and specifying a language' { + BeforeAll { + $mockConnectSQL = { + return New-Object -TypeName Object | + Add-Member -MemberType 'NoteProperty' -Name 'LoginMode' -Value 'Mixed' -PassThru | + Add-Member -MemberType 'ScriptProperty' -Name 'Logins' -Value { + # Mocks no existing logins. + return @{} + } -PassThru -Force + } + + Mock -CommandName Connect-SQL -MockWith $mockConnectSQL + Mock -CommandName New-SQLServerLogin + } + + It 'Should not throw and call the correct mocks' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $mockPassword = ConvertTo-SecureString -String 'P@ssw0rd-12P@ssw0rd-12' -AsPlainText -Force + + $mockSetTargetResourceParameters.Name = 'SqlLogin1' + $mockSetTargetResourceParameters.LoginType = 'SqlLogin' + $mockSetTargetResourceParameters.Language = 'Français' + $mockSetTargetResourceParameters.LoginCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockTestTargetResourceParameters.Name, $mockPassword) + + $null = Set-TargetResource @mockSetTargetResourceParameters -ErrorAction 'Stop' + } + + Should -Invoke -CommandName Connect-SQL -Exactly -Times 1 -Scope It + Should -Invoke -CommandName New-SQLServerLogin -ParameterFilter { + $Login.Name -eq 'SqlLogin1' -and $Login.Language -eq 'Français' + } -Exactly -Times 1 -Scope It + } + } + Context 'When creating a new login of type SqlLogin and specifying that it should be disabled' { BeforeAll { $mockConnectSQL = { @@ -1226,6 +1307,7 @@ Describe 'SqlLogin\Set-TargetResource' -Tag 'Set' { # Using the stub class Login from the SMO.cs file. $mockLoginObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Login' -ArgumentList ($null, 'Windows\Login1') $mockLoginObject.DefaultDatabase = 'master' + $mockLoginObject.Language = 'us_english' return @{ 'Windows\Login1' = $mockLoginObject @@ -1283,6 +1365,7 @@ Describe 'SqlLogin\Set-TargetResource' -Tag 'Set' { $mockLoginObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Login' -ArgumentList ($null, 'SqlLogin1') $mockLoginObject.LoginType = 'SqlLogin' $mockLoginObject.DefaultDatabase = 'master' + $mockLoginObject.Language = 'us_english' # Switch the mock value to the opposite of what should be the desired state. $mockLoginObject.PasswordPolicyEnforced = -not $MockPropertyValue $mockLoginObject.PasswordExpirationEnabled = -not $MockPropertyValue @@ -1386,6 +1469,10 @@ Describe 'SqlLogin\Set-TargetResource' -Tag 'Set' { MockPropertyName = 'DefaultDatabase' MockPropertyValue = 'Database1' } + @{ + MockPropertyName = 'Language' + MockPropertyValue = 'Français' + } ) { BeforeAll { $mockConnectSQL = { @@ -1395,6 +1482,7 @@ Describe 'SqlLogin\Set-TargetResource' -Tag 'Set' { $mockLoginObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Login' -ArgumentList ($null, 'SqlLogin1') $mockLoginObject.LoginType = 'SqlLogin' $mockLoginObject.DefaultDatabase = 'master' + $mockLoginObject.Language = 'us_english' return @{ 'SqlLogin1' = $mockLoginObject