diff --git a/CHANGELOG.md b/CHANGELOG.md index b0858125e5..11c74ab14c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - SqlScript - Added integration test configuration that creates script files and executes the resource in a single configuration using `DependsOn`. +- SqlLogin + - Added parameter `Sid` to allow setting the sid of the new login. ([issue #1470](https://github.com/dsccommunity/SqlServerDsc/issues/1470)) ## [17.5.1] - 2026-02-05 diff --git a/source/DSCResources/DSC_SqlLogin/DSC_SqlLogin.psm1 b/source/DSCResources/DSC_SqlLogin/DSC_SqlLogin.psm1 index a691294c09..2f0953d921 100644 --- a/source/DSCResources/DSC_SqlLogin/DSC_SqlLogin.psm1 +++ b/source/DSCResources/DSC_SqlLogin/DSC_SqlLogin.psm1 @@ -72,6 +72,7 @@ function Get-TargetResource Disabled = $login.IsDisabled DefaultDatabase = $login.DefaultDatabase Language = $login.Language + Sid = $login.Sid } if ($login.LoginType -eq 'SqlLogin') @@ -124,6 +125,9 @@ function Get-TargetResource .PARAMETER Language Specifies the default language for the login. + + .PARAMETER Sid + Specifies the security identifier (SID) for the login. Only applies to SQL Logins. The value should be a hexadecimal string (e.g. '0x1234...'). #> function Set-TargetResource { @@ -187,7 +191,12 @@ function Set-TargetResource [Parameter()] [System.String] - $Language + $Language, + + [Parameter()] + [ValidatePattern('^0x([0-9A-Fa-f]{2})+$')] + [System.String] + $Sid ) $serverObject = Connect-SQL -ServerName $ServerName -InstanceName $InstanceName -ErrorAction 'Stop' @@ -272,7 +281,8 @@ function Set-TargetResource } if ( ( $PSBoundParameters.ContainsKey('DefaultDatabase') -and ($login.DefaultDatabase -ne $DefaultDatabase) ) -or - ( $PSBoundParameters.ContainsKey('Language') -and $login.Language -ne $Language ) ) + ( $PSBoundParameters.ContainsKey('Language') -and ($login.Language -ne $Language) ) + ) { if ( $PSBoundParameters.ContainsKey('DefaultDatabase') ) { @@ -336,6 +346,11 @@ function Set-TargetResource $LoginCreateOptions = [Microsoft.SqlServer.Management.Smo.LoginCreateOptions]::None } + if ( $PSBoundParameters.ContainsKey('Sid') ) + { + $login.Sid = ([byte[]] -split ( $Sid -replace '^0x', '' -replace '..', '0x$& ')) + } + New-SQLServerLogin -Login $login -LoginCreateOptions $LoginCreateOptions -SecureString $LoginCredential.Password -ErrorAction 'Stop' } @@ -356,7 +371,8 @@ function Set-TargetResource } if ( ( $PSBoundParameters.ContainsKey('DefaultDatabase') -and ($login.DefaultDatabase -ne $DefaultDatabase) ) -or - ( $PSBoundParameters.ContainsKey('Language') -and $login.Language -ne $Language ) ) + ( $PSBoundParameters.ContainsKey('Language') -and ($login.Language -ne $Language) ) + ) { # Set the default database if specified if ( $PSBoundParameters.ContainsKey('DefaultDatabase') ) @@ -428,6 +444,11 @@ function Set-TargetResource .PARAMETER Language Specifies the default language for the login. + + .PARAMETER Sid + Specifies the security identifier (SID) for the login. Only applies to SQL Logins. The value should be a hexadecimal string (e.g. '0x1234...'). + + Not currently used in Test-TargetResource to enforce Sid. #> function Test-TargetResource { @@ -491,7 +512,12 @@ function Test-TargetResource [Parameter()] [System.String] - $Language + $Language, + + [Parameter()] + [ValidatePattern('^0x([0-9A-Fa-f]{2})+$')] + [System.String] + $Sid ) Write-Verbose -Message ( diff --git a/source/DSCResources/DSC_SqlLogin/DSC_SqlLogin.schema.mof b/source/DSCResources/DSC_SqlLogin/DSC_SqlLogin.schema.mof index 5ef638f33d..0a57563854 100644 --- a/source/DSCResources/DSC_SqlLogin/DSC_SqlLogin.schema.mof +++ b/source/DSCResources/DSC_SqlLogin/DSC_SqlLogin.schema.mof @@ -15,4 +15,5 @@ class DSC_SqlLogin : OMI_BaseResource [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; + [Write, Description("Specifies the security identifier (SID) for the login. Only applies to _SQL Logins_. The value should be a hexadecimal string (e.g. `'0x1234...'`).")] String Sid; }; diff --git a/source/Examples/Resources/SqlLogin/1-AddSqlLogin.ps1 b/source/Examples/Resources/SqlLogin/1-AddSqlLogin.ps1 new file mode 100644 index 0000000000..a40e6dc352 --- /dev/null +++ b/source/Examples/Resources/SqlLogin/1-AddSqlLogin.ps1 @@ -0,0 +1,53 @@ +<# + .DESCRIPTION + This example shows how to ensure that the SQL logins 'SqlLogin' and + 'SqlLogin2' exist, where 'SqlLogin2' is created with an explicit SID. +#> + +Configuration Example +{ + param + ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $SqlAdministratorCredential, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $LoginCredential + ) + + Import-DscResource -ModuleName 'SqlServerDsc' + + node localhost + { + SqlLogin 'Add_SqlLogin' + { + Ensure = 'Present' + Name = 'SqlLogin' + LoginType = 'SqlLogin' + ServerName = 'TestServer.company.local' + InstanceName = 'DSC' + LoginCredential = $LoginCredential + LoginMustChangePassword = $false + LoginPasswordExpirationEnabled = $true + LoginPasswordPolicyEnforced = $true + PsDscRunAsCredential = $SqlAdministratorCredential + } + + SqlLogin 'Add_SqlLogin_Set_Login_Sid' + { + Ensure = 'Present' + Name = 'SqlLogin2' + LoginType = 'SqlLogin' + ServerName = 'TestServer.company.local' + InstanceName = 'DSC' + LoginCredential = $LoginCredential + LoginMustChangePassword = $false + LoginPasswordExpirationEnabled = $true + LoginPasswordPolicyEnforced = $true + PsDscRunAsCredential = $SqlAdministratorCredential + Sid = '0x5283175DBF354E508FB7582940E87500' + } + } +} diff --git a/source/Examples/Resources/SqlLogin/1-AddLogin.ps1 b/source/Examples/Resources/SqlLogin/1-AddWindowsUser.ps1 similarity index 69% rename from source/Examples/Resources/SqlLogin/1-AddLogin.ps1 rename to source/Examples/Resources/SqlLogin/1-AddWindowsUser.ps1 index fea5783409..3fbc844d58 100644 --- a/source/Examples/Resources/SqlLogin/1-AddLogin.ps1 +++ b/source/Examples/Resources/SqlLogin/1-AddWindowsUser.ps1 @@ -1,7 +1,8 @@ <# .DESCRIPTION This example shows how to ensure that the Windows user 'CONTOSO\WindowsUser', - Windows group 'CONTOSO\WindowsGroup', and the SQL Login 'SqlLogin' exists. + 'CONTOSO\WindowsUser2', 'CONTOSO\WindowsUser3', + and Windows group 'CONTOSO\WindowsGroup' exists. #> Configuration Example @@ -10,11 +11,7 @@ Configuration Example ( [Parameter(Mandatory = $true)] [System.Management.Automation.PSCredential] - $SqlAdministratorCredential, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $LoginCredential + $SqlAdministratorCredential ) Import-DscResource -ModuleName 'SqlServerDsc' @@ -62,19 +59,5 @@ Configuration Example InstanceName = 'DSC' PsDscRunAsCredential = $SqlAdministratorCredential } - - SqlLogin 'Add_SqlLogin' - { - Ensure = 'Present' - Name = 'SqlLogin' - LoginType = 'SqlLogin' - ServerName = 'TestServer.company.local' - InstanceName = 'DSC' - LoginCredential = $LoginCredential - LoginMustChangePassword = $false - LoginPasswordExpirationEnabled = $true - LoginPasswordPolicyEnforced = $true - PsDscRunAsCredential = $SqlAdministratorCredential - } } } diff --git a/tests/Unit/DSC_SqlLogin.Tests.ps1 b/tests/Unit/DSC_SqlLogin.Tests.ps1 index 08fff817c9..765e5d2a31 100644 --- a/tests/Unit/DSC_SqlLogin.Tests.ps1 +++ b/tests/Unit/DSC_SqlLogin.Tests.ps1 @@ -117,6 +117,7 @@ Describe 'SqlLogin\Get-TargetResource' -Tag 'Get' { 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 'Sid' -Value ([byte[]] -split ('B76150A66B38F64FAE9470091789AA66' -replace '..', '0x$& ')) -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 | @@ -147,6 +148,7 @@ Describe 'SqlLogin\Get-TargetResource' -Tag 'Get' { $result.Disabled | Should -BeTrue $result.DefaultDatabase | Should -Be 'master' $result.Language | Should -Be 'us_english' + $result.Sid | Should -Be ([byte[]] -split ('B76150A66B38F64FAE9470091789AA66' -replace '..', '0x$& ')) if ($MockLoginType -eq 'SqlLogin') { @@ -200,6 +202,7 @@ Describe 'SqlLogin\Get-TargetResource' -Tag 'Get' { 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 'Sid' -Value ([byte[]] -split ('B76150A66B38F64FAE9470091789AA66' -replace '..', '0x$& ')) -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 | @@ -230,6 +233,7 @@ Describe 'SqlLogin\Get-TargetResource' -Tag 'Get' { $result.Disabled | Should -BeFalse $result.DefaultDatabase | Should -BeNullOrEmpty $result.Language | Should -BeNullOrEmpty + $result.Sid | Should -BeNullOrEmpty if ($MockLoginType -eq 'SqlLogin') { @@ -353,7 +357,7 @@ Describe 'SqlLogin\Test-TargetResource' -Tag 'Test' { } } - Context 'When the parmeter Disabled is set to True for a SQL login (and the account is disabled)' { + Context 'When the parameter Disabled is set to True for a SQL login (and the account is disabled)' { BeforeAll { Mock -CommandName Connect-SQL -MockWith { $mockAccountDisabledException = New-Object -TypeName 'System.Exception' -ArgumentList 'Account disabled' @@ -548,7 +552,7 @@ Describe 'SqlLogin\Test-TargetResource' -Tag 'Test' { } Context 'When evaluating the login credentials for a SQL login' { - Context 'When the parmeter Disabled is set to True for a SQL login (if login fails the login credentials are not in desired state)' { + Context 'When the parameter Disabled is set to True for a SQL login (if login fails the login credentials are not in desired state)' { BeforeAll { Mock -CommandName Connect-SQL -MockWith { $mockLoginFailedException = New-Object System.Exception 'Login failed' @@ -586,7 +590,7 @@ Describe 'SqlLogin\Test-TargetResource' -Tag 'Test' { } } - Context 'When the parmeter Disabled is set to False for a SQL login (if login fails the login credentials are not in desired state)' { + Context 'When the parameter Disabled is set to False for a SQL login (if login fails the login credentials are not in desired state)' { BeforeAll { Mock -CommandName Connect-SQL -MockWith { $mockLoginFailedException = New-Object System.Exception 'Login failed' @@ -623,7 +627,7 @@ Describe 'SqlLogin\Test-TargetResource' -Tag 'Test' { } } - Context 'When the parmeter Disabled is set to True for a SQL login (and an unexpected exception is thrown)' { + Context 'When the parameter Disabled is set to True for a SQL login (and an unexpected exception is thrown)' { BeforeAll { Mock -CommandName Connect-SQL -MockWith { $mockException = New-Object System.Exception 'Something went wrong' @@ -882,7 +886,7 @@ Describe 'SqlLogin\Set-TargetResource' -Tag 'Set' { $mockSetTargetResourceParameters.Name = 'SqlLogin1' $mockSetTargetResourceParameters.LoginType = 'SqlLogin' $mockSetTargetResourceParameters.LoginMustChangePassword = $true - $mockSetTargetResourceParameters.LoginCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockTestTargetResourceParameters.Name, $mockPassword) + $mockSetTargetResourceParameters.LoginCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockSetTargetResourceParameters.Name, $mockPassword) $null = Set-TargetResource @mockSetTargetResourceParameters -ErrorAction 'Stop' } @@ -930,7 +934,7 @@ Describe 'SqlLogin\Set-TargetResource' -Tag 'Set' { $mockSetTargetResourceParameters.Name = 'SqlLogin1' $mockSetTargetResourceParameters.LoginType = 'SqlLogin' $mockSetTargetResourceParameters.LoginMustChangePassword = $false - $mockSetTargetResourceParameters.LoginCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockTestTargetResourceParameters.Name, $mockPassword) + $mockSetTargetResourceParameters.LoginCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockSetTargetResourceParameters.Name, $mockPassword) $null = Set-TargetResource @mockSetTargetResourceParameters -ErrorAction 'Stop' } @@ -974,7 +978,7 @@ Describe 'SqlLogin\Set-TargetResource' -Tag 'Set' { $mockSetTargetResourceParameters.Name = 'SqlLogin1' $mockSetTargetResourceParameters.LoginType = 'SqlLogin' $mockSetTargetResourceParameters.DefaultDatabase = 'NewDatabase' - $mockSetTargetResourceParameters.LoginCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockTestTargetResourceParameters.Name, $mockPassword) + $mockSetTargetResourceParameters.LoginCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockSetTargetResourceParameters.Name, $mockPassword) $null = Set-TargetResource @mockSetTargetResourceParameters -ErrorAction 'Stop' } @@ -1010,7 +1014,7 @@ Describe 'SqlLogin\Set-TargetResource' -Tag 'Set' { $mockSetTargetResourceParameters.Name = 'SqlLogin1' $mockSetTargetResourceParameters.LoginType = 'SqlLogin' $mockSetTargetResourceParameters.Language = 'Français' - $mockSetTargetResourceParameters.LoginCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockTestTargetResourceParameters.Name, $mockPassword) + $mockSetTargetResourceParameters.LoginCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockSetTargetResourceParameters.Name, $mockPassword) $null = Set-TargetResource @mockSetTargetResourceParameters -ErrorAction 'Stop' } @@ -1022,6 +1026,42 @@ Describe 'SqlLogin\Set-TargetResource' -Tag 'Set' { } } + Context 'When creating a new login of type SqlLogin and specifying a Sid' { + 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.Sid = '0x17442048803848B58686603376A84216' + $mockSetTargetResourceParameters.LoginCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockSetTargetResourceParameters.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 @(Compare-Object $login.Sid ([byte[]] -split ( '17442048803848B58686603376A84216' -replace '..', '0x$& ')) -SyncWindow 0).Length -eq 0 + } -Exactly -Times 1 -Scope It + } + } + Context 'When creating a new login of type SqlLogin and specifying that it should be disabled' { BeforeAll { $mockConnectSQL = { @@ -1067,7 +1107,7 @@ Describe 'SqlLogin\Set-TargetResource' -Tag 'Set' { $mockSetTargetResourceParameters.Name = 'SqlLogin1' $mockSetTargetResourceParameters.LoginType = 'SqlLogin' $mockSetTargetResourceParameters.Disabled = $true - $mockSetTargetResourceParameters.LoginCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockTestTargetResourceParameters.Name, $mockPassword) + $mockSetTargetResourceParameters.LoginCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockSetTargetResourceParameters.Name, $mockPassword) $null = Set-TargetResource @mockSetTargetResourceParameters -ErrorAction 'Stop' @@ -1104,7 +1144,7 @@ Describe 'SqlLogin\Set-TargetResource' -Tag 'Set' { $mockSetTargetResourceParameters.Name = 'SqlLogin1' $mockSetTargetResourceParameters.LoginType = 'SqlLogin' - $mockSetTargetResourceParameters.LoginCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockTestTargetResourceParameters.Name, $mockPassword) + $mockSetTargetResourceParameters.LoginCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockSetTargetResourceParameters.Name, $mockPassword) $mockErrorMessage = $script:localizedData.IncorrectLoginMode -f 'localhost', 'MSSQLSERVER', 'Integrated' @@ -1575,7 +1615,7 @@ Describe 'SqlLogin\Set-TargetResource' -Tag 'Set' { $mockSetTargetResourceParameters.Name = 'SqlLogin1' $mockSetTargetResourceParameters.LoginType = 'SqlLogin' - $mockSetTargetResourceParameters.LoginCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockTestTargetResourceParameters.Name, $mockPassword) + $mockSetTargetResourceParameters.LoginCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockSetTargetResourceParameters.Name, $mockPassword) $null = Set-TargetResource @mockSetTargetResourceParameters -ErrorAction 'Stop' } diff --git a/tests/Unit/Stubs/SMO.cs b/tests/Unit/Stubs/SMO.cs index a29567d860..90d6133033 100644 --- a/tests/Unit/Stubs/SMO.cs +++ b/tests/Unit/Stubs/SMO.cs @@ -774,6 +774,7 @@ public void Enable() public string Certificate; public string AsymmetricKey; public string Language; + public byte[] Sid; public void Drop() {