Skip to content

Commit f2da1a2

Browse files
authored
Remove-SqlDscLogin: Add KillActiveSessions parameter (#2379)
1 parent a45049f commit f2da1a2

File tree

7 files changed

+273
-18
lines changed

7 files changed

+273
-18
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5151
SMO `Database.SetOffline()`. Supports Server and Database pipeline input;
5252
includes `Force` to disconnect active users
5353
([issue #2192](https://github.com/dsccommunity/SqlServerDsc/issues/2192)).
54+
- `Remove-SqlDscLogin`
55+
- Added parameter `-KillActiveSessions` to automatically terminate any active
56+
sessions for a login before dropping it
57+
([issue #2372](https://github.com/dsccommunity/SqlServerDsc/issues/2372)).
5458

5559
### Changed
5660

source/Public/Remove-SqlDscLogin.ps1

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
.PARAMETER Name
1515
Specifies the name of the server login to be removed.
1616
17+
.PARAMETER KillActiveSessions
18+
Specifies that any active sessions for the login should be terminated
19+
before attempting to drop the login. This is useful when the login has
20+
active connections that would otherwise prevent the drop operation.
21+
1722
.PARAMETER Force
1823
Specifies that the login should be removed without any confirmation.
1924
@@ -23,6 +28,7 @@
2328
been modified outside of the **ServerObject**, for example through T-SQL.
2429
But on instances with a large number of logins it might be better to make
2530
sure the **ServerObject** is recent enough, or pass in **LoginObject**.
31+
2632
.EXAMPLE
2733
$serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance'
2834
$loginObject = $serverObject | Get-SqlDscLogin -Name 'MyLogin'
@@ -36,6 +42,13 @@
3642
3743
Removes the login named **MyLogin**.
3844
45+
.EXAMPLE
46+
$serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance'
47+
$serverObject | Remove-SqlDscLogin -Name 'MyLogin' -KillActiveSessions -Force
48+
49+
Removes the login named **MyLogin** after terminating any active sessions
50+
for the login.
51+
3952
.INPUTS
4053
`Microsoft.SqlServer.Management.Smo.Server`
4154
@@ -68,6 +81,10 @@ function Remove-SqlDscLogin
6881
[System.String]
6982
$Name,
7083

84+
[Parameter()]
85+
[System.Management.Automation.SwitchParameter]
86+
$KillActiveSessions,
87+
7188
[Parameter()]
7289
[System.Management.Automation.SwitchParameter]
7390
$Force,
@@ -106,6 +123,44 @@ function Remove-SqlDscLogin
106123

107124
if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage))
108125
{
126+
if ($KillActiveSessions.IsPresent)
127+
{
128+
$serverObjectToUse = $LoginObject.Parent
129+
130+
Write-Debug -Message (
131+
$script:localizedData.Login_Remove_KillingActiveSessions -f $LoginObject.Name
132+
)
133+
134+
$processes = $serverObjectToUse.EnumProcesses($LoginObject.Name)
135+
136+
$originalErrorActionPreference = $ErrorActionPreference
137+
138+
$ErrorActionPreference = 'Stop'
139+
140+
# cSpell:ignore Spid
141+
foreach ($process in $processes.Rows)
142+
{
143+
Write-Debug -Message (
144+
$script:localizedData.Login_Remove_KillingProcess -f $process.Spid, $LoginObject.Name
145+
)
146+
147+
# Ignore errors if process already terminated.
148+
try
149+
{
150+
$serverObjectToUse.KillProcess($process.Spid)
151+
}
152+
catch
153+
{
154+
# Ignore error if process already terminated.
155+
Write-Debug -Message (
156+
$script:localizedData.Login_Remove_KillProcessFailed -f $process.Spid, $_.Exception.Message
157+
)
158+
}
159+
}
160+
161+
$ErrorActionPreference = $originalErrorActionPreference
162+
}
163+
109164
try
110165
{
111166
$originalErrorActionPreference = $ErrorActionPreference

source/en-US/SqlServerDsc.strings.psd1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ ConvertFrom-StringData @'
101101
# This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages.
102102
Login_Remove_ShouldProcessCaption = Remove login on instance
103103
Login_Remove_Failed = Removal of the login '{0}' failed. (RSDL0001)
104+
Login_Remove_KillingActiveSessions = Killing active sessions for login '{0}'. (RSDL0002)
105+
Login_Remove_KillingProcess = Killing process with SPID '{0}' for login '{1}'. (RSDL0003)
106+
Login_Remove_KillProcessFailed = Failed to kill process with SPID '{0}'. It may have already terminated. Error: {1} (RSDL0004)
104107
105108
## Enable-SqlDscLogin
106109
Login_Enable_ShouldProcessVerboseDescription = Enabling the login '{0}' on the instance '{1}'.

tests/Integration/Commands/Remove-SqlDscLogin.Integration.Tests.ps1

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,72 @@ Describe 'Remove-SqlDscLogin' -Tag @('Integration_SQL2017', 'Integration_SQL2019
157157
$loginExists | Should -BeFalse
158158
}
159159
}
160+
161+
Context 'When using the KillActiveSessions parameter' {
162+
BeforeAll {
163+
$script:testLoginName4 = 'TestRemoveLogin4'
164+
$script:testLoginPassword4 = ConvertTo-SecureString -String 'P@ssw0rd4!' -AsPlainText -Force
165+
$script:testCredential4 = [System.Management.Automation.PSCredential]::new($script:testLoginName4, $script:testLoginPassword4)
166+
}
167+
168+
AfterEach {
169+
# Clean up any remaining sessions
170+
if ($script:activeConnection)
171+
{
172+
Disconnect-SqlDscDatabaseEngine -ServerObject $script:activeConnection -ErrorAction 'SilentlyContinue'
173+
$script:activeConnection = $null
174+
}
175+
176+
# Clean up the login if it still exists
177+
$loginExists = Test-SqlDscIsLogin -ServerObject $script:serverObject -Name $script:testLoginName4
178+
179+
if ($loginExists)
180+
{
181+
# Use KillActiveSessions to ensure cleanup
182+
$script:serverObject | Remove-SqlDscLogin -Name $script:testLoginName4 -KillActiveSessions -Force -ErrorAction 'SilentlyContinue'
183+
}
184+
}
185+
186+
It 'Should remove a login with active sessions when using KillActiveSessions parameter' {
187+
# Create the test login
188+
$null = $script:serverObject | New-SqlDscLogin -Name $script:testLoginName4 -SqlLogin -SecurePassword $script:testLoginPassword4 -Force
189+
190+
# Verify the login exists
191+
$loginExists = Test-SqlDscIsLogin -ServerObject $script:serverObject -Name $script:testLoginName4
192+
$loginExists | Should -BeTrue
193+
194+
# Connect using the test login to create an active session
195+
$script:activeConnection = Connect-SqlDscDatabaseEngine -InstanceName $script:mockInstanceName -LoginType 'SqlLogin' -Credential $script:testCredential4 -ErrorAction 'Stop'
196+
197+
# Verify there is an active session for this login
198+
$processes = $script:serverObject.EnumProcesses($script:testLoginName4)
199+
$processes.Rows.Count | Should -BeGreaterThan 0 -Because 'There should be at least one active session for the login'
200+
201+
# Remove the login with KillActiveSessions - should succeed
202+
$script:serverObject | Remove-SqlDscLogin -Name $script:testLoginName4 -KillActiveSessions -Force
203+
204+
# Verify the login is removed
205+
$loginExists = Test-SqlDscIsLogin -ServerObject $script:serverObject -Name $script:testLoginName4
206+
$loginExists | Should -BeFalse
207+
}
208+
209+
It 'Should fail to remove a login with active sessions when not using KillActiveSessions parameter' {
210+
# Create the test login
211+
$null = $script:serverObject | New-SqlDscLogin -Name $script:testLoginName4 -SqlLogin -SecurePassword $script:testLoginPassword4 -Force
212+
213+
# Verify the login exists
214+
$loginExists = Test-SqlDscIsLogin -ServerObject $script:serverObject -Name $script:testLoginName4
215+
$loginExists | Should -BeTrue
216+
217+
# Connect using the test login to create an active session
218+
$script:activeConnection = Connect-SqlDscDatabaseEngine -InstanceName $script:mockInstanceName -LoginType 'SqlLogin' -Credential $script:testCredential4 -ErrorAction 'Stop'
219+
220+
# Verify there is an active session for this login
221+
$processes = $script:serverObject.EnumProcesses($script:testLoginName4)
222+
$processes.Rows.Count | Should -BeGreaterThan 0 -Because 'There should be at least one active session for the login'
223+
224+
# Try to remove the login without KillActiveSessions - should fail
225+
{ $script:serverObject | Remove-SqlDscLogin -Name $script:testLoginName4 -Force -ErrorAction 'Stop' } | Should -Throw
226+
}
227+
}
160228
}

tests/Integration/Commands/Restore-SqlDscDatabase.Integration.Tests.ps1

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,22 +1044,7 @@ WITH NOINIT, NOSKIP, REWIND, NOUNLOAD, STATS = 10;
10441044

10451045
if ($loginObject)
10461046
{
1047-
# Kill any active sessions for this login before dropping using SMO
1048-
$processes = $script:serverObject.EnumProcesses($script:lowPrivLoginName)
1049-
1050-
foreach ($process in $processes)
1051-
{
1052-
try
1053-
{
1054-
$script:serverObject.KillProcess($process.Spid)
1055-
}
1056-
catch
1057-
{
1058-
# Ignore errors if process already terminated
1059-
}
1060-
}
1061-
1062-
$null = Remove-SqlDscLogin -LoginObject $loginObject -Force -ErrorAction 'SilentlyContinue'
1047+
$null = Remove-SqlDscLogin -LoginObject $loginObject -KillActiveSessions -Force -ErrorAction 'SilentlyContinue'
10631048
}
10641049
}
10651050

tests/Unit/Public/Remove-SqlDscLogin.Tests.ps1

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ Describe 'Remove-SqlDscLogin' -Tag 'Public' {
5353
It 'Should have the correct parameters in parameter set <MockParameterSetName>' -ForEach @(
5454
@{
5555
MockParameterSetName = 'ServerObject'
56-
MockExpectedParameters = '-ServerObject <Server> -Name <string> [-Force] [-Refresh] [-WhatIf] [-Confirm] [<CommonParameters>]'
56+
MockExpectedParameters = '-ServerObject <Server> -Name <string> [-KillActiveSessions] [-Force] [-Refresh] [-WhatIf] [-Confirm] [<CommonParameters>]'
5757
}
5858
@{
5959
MockParameterSetName = 'LoginObject'
60-
MockExpectedParameters = '-LoginObject <Login> [-Force] [-WhatIf] [-Confirm] [<CommonParameters>]'
60+
MockExpectedParameters = '-LoginObject <Login> [-KillActiveSessions] [-Force] [-WhatIf] [-Confirm] [<CommonParameters>]'
6161
}
6262
) {
6363
$result = (Get-Command -Name 'Remove-SqlDscLogin').ParameterSets |
@@ -285,4 +285,125 @@ Describe 'Remove-SqlDscLogin' -Tag 'Public' {
285285
{ Remove-SqlDscLogin -Force @mockDefaultParameters } | Should -Throw -ExpectedMessage '*Removal of the login ''TestLogin'' failed*'
286286
}
287287
}
288+
289+
Context 'When using parameter KillActiveSessions' {
290+
BeforeAll {
291+
$mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'
292+
$mockServerObject.InstanceName = 'TestInstance'
293+
}
294+
295+
BeforeEach {
296+
$script:mockMethodDropCallCount = 0
297+
}
298+
299+
Context 'When there are active sessions for the login' {
300+
BeforeAll {
301+
# The SMO stub EnumProcesses returns a DataTable with SPID 51 for any login name
302+
$script:mockLoginObjectWithProcesses = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Login' -ArgumentList @(
303+
$mockServerObject,
304+
'TestLogin'
305+
) |
306+
Add-Member -MemberType 'ScriptMethod' -Name 'Drop' -Value {
307+
$script:mockMethodDropCallCount += 1
308+
} -PassThru -Force
309+
310+
$script:mockDefaultParametersWithProcesses = @{
311+
LoginObject = $script:mockLoginObjectWithProcesses
312+
KillActiveSessions = $true
313+
}
314+
}
315+
316+
It 'Should kill active sessions and drop the login' {
317+
Remove-SqlDscLogin -Force @mockDefaultParametersWithProcesses
318+
319+
$script:mockMethodDropCallCount | Should -Be 1
320+
}
321+
}
322+
323+
Context 'When using WhatIf with KillActiveSessions' {
324+
BeforeAll {
325+
$script:mockLoginObjectWhatIf = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Login' -ArgumentList @(
326+
$mockServerObject,
327+
'TestLogin'
328+
) |
329+
Add-Member -MemberType 'ScriptMethod' -Name 'Drop' -Value {
330+
$script:mockMethodDropCallCount += 1
331+
} -PassThru -Force
332+
333+
$script:mockDefaultParametersWhatIf = @{
334+
LoginObject = $script:mockLoginObjectWhatIf
335+
KillActiveSessions = $true
336+
}
337+
}
338+
339+
It 'Should not kill sessions or drop the login' {
340+
Remove-SqlDscLogin -WhatIf @mockDefaultParametersWhatIf
341+
342+
$script:mockMethodDropCallCount | Should -Be 0
343+
}
344+
}
345+
346+
Context 'When using ServerObject parameter set with KillActiveSessions' {
347+
BeforeAll {
348+
$mockServerObjectWithProcesses = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'
349+
$mockServerObjectWithProcesses.InstanceName = 'TestInstance'
350+
351+
Mock -CommandName Get-SqlDscLogin -MockWith {
352+
return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Login' -ArgumentList @(
353+
$mockServerObjectWithProcesses,
354+
'TestLogin'
355+
) |
356+
Add-Member -MemberType 'ScriptMethod' -Name 'Drop' -Value {
357+
$script:mockMethodDropCallCount += 1
358+
} -PassThru -Force
359+
}
360+
}
361+
362+
It 'Should kill active sessions and drop the login' {
363+
Remove-SqlDscLogin -ServerObject $mockServerObjectWithProcesses -Name 'TestLogin' -KillActiveSessions -Force
364+
365+
$script:mockMethodDropCallCount | Should -Be 1
366+
}
367+
368+
It 'Should not kill sessions or drop the login when using WhatIf' {
369+
Remove-SqlDscLogin -ServerObject $mockServerObjectWithProcesses -Name 'TestLogin' -KillActiveSessions -Force -WhatIf
370+
371+
$script:mockMethodDropCallCount | Should -Be 0
372+
}
373+
}
374+
375+
Context 'When EnumProcesses returns no active sessions' {
376+
BeforeAll {
377+
# Create a server object that returns an empty DataTable from EnumProcesses
378+
$mockServerObjectNoProcesses = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'
379+
$mockServerObjectNoProcesses.InstanceName = 'TestInstance'
380+
381+
# Override EnumProcesses to return an empty DataTable
382+
$mockServerObjectNoProcesses | Add-Member -MemberType 'ScriptMethod' -Name 'EnumProcesses' -Value {
383+
param ($loginName)
384+
385+
$dataTable = New-Object -TypeName 'System.Data.DataTable'
386+
$null = $dataTable.Columns.Add('Spid', [System.Int32])
387+
$null = $dataTable.Columns.Add('Login', [System.String])
388+
389+
# Return empty DataTable (no rows)
390+
return $dataTable
391+
} -Force
392+
393+
$script:mockLoginObjectNoProcesses = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Login' -ArgumentList @(
394+
$mockServerObjectNoProcesses,
395+
'TestLogin'
396+
) |
397+
Add-Member -MemberType 'ScriptMethod' -Name 'Drop' -Value {
398+
$script:mockMethodDropCallCount += 1
399+
} -PassThru -Force
400+
}
401+
402+
It 'Should drop the login without attempting to kill any sessions' {
403+
Remove-SqlDscLogin -LoginObject $script:mockLoginObjectNoProcesses -KillActiveSessions -Force
404+
405+
$script:mockMethodDropCallCount | Should -Be 1
406+
}
407+
}
408+
}
288409
}

tests/Unit/Stubs/SMO.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,25 @@ public void KillAllProcesses( string databaseName )
562562
{
563563
}
564564

565+
public void KillProcess( int processId )
566+
{
567+
}
568+
569+
public DataTable EnumProcesses( string loginName )
570+
{
571+
var dataTable = new DataTable();
572+
dataTable.Columns.Add("Spid", typeof(int));
573+
dataTable.Columns.Add("Login", typeof(string));
574+
575+
// Return a row with SPID 51 for any login name
576+
var row = dataTable.NewRow();
577+
row["Spid"] = 51;
578+
row["Login"] = loginName;
579+
dataTable.Rows.Add(row);
580+
581+
return dataTable;
582+
}
583+
565584
// Property for SQL Agent support
566585
public Microsoft.SqlServer.Management.Smo.Agent.JobServer JobServer { get; set; }
567586

0 commit comments

Comments
 (0)