Skip to content

Commit 11d39a0

Browse files
authored
Add integration test for Get-SqlDscDatabasePermission command (#2253)
1 parent 4597f6d commit 11d39a0

File tree

8 files changed

+431
-2
lines changed

8 files changed

+431
-2
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
### Added
99

10+
- Added integration tests for `Get-SqlDscDatabasePermission` command to ensure
11+
database permission retrieval functions correctly in real environments
12+
[issue #2221](https://github.com/dsccommunity/SqlServerDsc/issues/2221).
1013
- Added integration tests for `Get-SqlDscManagedComputer` command to ensure it
1114
functions correctly in real environments
1215
[issue #2220](https://github.com/dsccommunity/SqlServerDsc/issues/2220).
@@ -82,6 +85,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8285
correctly in real environments
8386
[issue #2214](https://github.com/dsccommunity/SqlServerDsc/issues/2214).
8487

88+
### Changed
89+
90+
- `Test-SqlDscIsDatabasePrincipal` and `Get-SqlDscDatabasePermission`
91+
- Added `Refresh` parameter to refresh SMO collections before checking
92+
database principals, addressing issues with custom database roles created
93+
via T-SQL that aren't immediately visible to SMO. The refresh logic is
94+
optimized to only refresh collections that will be used based on exclude
95+
parameters, improving performance on databases with large numbers of principals
96+
([issue #2221](https://github.com/dsccommunity/SqlServerDsc/issues/2221)).
97+
8598
### Fixed
8699

87100
- `Add-SqlDscTraceFlag` and `Remove-SqlDscTraceFlag`

azure-pipelines.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ stages:
327327
'tests/Integration/Commands/New-SqlDscDatabase.Integration.Tests.ps1'
328328
'tests/Integration/Commands/Set-SqlDscDatabase.Integration.Tests.ps1'
329329
'tests/Integration/Commands/Test-SqlDscDatabase.Integration.Tests.ps1'
330+
'tests/Integration/Commands/Get-SqlDscDatabasePermission.Integration.Tests.ps1'
330331
'tests/Integration/Commands/Invoke-SqlDscQuery.Integration.Tests.ps1'
331332
'tests/Integration/Commands/Set-SqlDscDatabasePermission.Integration.Tests.ps1'
332333
'tests/Integration/Commands/ConvertTo-SqlDscDatabasePermission.Integration.Tests.ps1'

source/Public/Get-SqlDscDatabasePermission.ps1

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
1515
Specifies the name of the database principal for which the permissions are
1616
returned.
1717
18+
.PARAMETER Refresh
19+
Specifies that the database's principal collections (Users, Roles, and
20+
ApplicationRoles) should be refreshed before testing if the principal exists.
21+
This is helpful when principals could have been modified outside of the
22+
**ServerObject**, for example through T-SQL. But on databases with a large
23+
amount of principals it might be better to make sure the **ServerObject**
24+
is recent enough.
25+
1826
.OUTPUTS
1927
[Microsoft.SqlServer.Management.Smo.DatabasePermissionInfo[]]
2028
@@ -24,6 +32,13 @@
2432
2533
Get the permissions for the principal 'MyPrincipal'.
2634
35+
.EXAMPLE
36+
$serverInstance = Connect-SqlDscDatabaseEngine
37+
Get-SqlDscDatabasePermission -ServerObject $serverInstance -DatabaseName 'MyDatabase' -Name 'MyPrincipal' -Refresh
38+
39+
Get the permissions for the principal 'MyPrincipal'. The database's principal
40+
collections are refreshed before testing if the principal exists.
41+
2742
.NOTES
2843
This command excludes fixed roles like _db_datareader_ by default, and will
2944
always return `$null` if a fixed role is specified as **Name**.
@@ -52,7 +67,11 @@ function Get-SqlDscDatabasePermission
5267

5368
[Parameter(Mandatory = $true)]
5469
[System.String]
55-
$Name
70+
$Name,
71+
72+
[Parameter()]
73+
[System.Management.Automation.SwitchParameter]
74+
$Refresh
5675
)
5776

5877
# cSpell: ignore GSDDP
@@ -76,6 +95,11 @@ function Get-SqlDscDatabasePermission
7695
ExcludeFixedRoles = $true
7796
}
7897

98+
if ($Refresh.IsPresent)
99+
{
100+
$testSqlDscIsDatabasePrincipalParameters.Refresh = $true
101+
}
102+
79103
$isDatabasePrincipal = Test-SqlDscIsDatabasePrincipal @testSqlDscIsDatabasePrincipalParameters
80104

81105
if ($isDatabasePrincipal)

source/Public/Test-SqlDscIsDatabasePrincipal.ps1

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@
2828
Specifies that fixed application roles should not be evaluated for the
2929
specified name.
3030
31+
.PARAMETER Refresh
32+
Specifies that the database's principal collections (Users, Roles, and
33+
ApplicationRoles) should be refreshed before testing if the principal exists.
34+
This is helpful when principals could have been modified outside of the
35+
**ServerObject**, for example through T-SQL. But on databases with a large
36+
amount of principals it might be better to make sure the **ServerObject**
37+
is recent enough. When exclude parameters are specified (e.g., **ExcludeUsers**,
38+
**ExcludeRoles**, **ExcludeApplicationRoles**), only the collections that will
39+
be used are refreshed to improve performance.
40+
3141
.OUTPUTS
3242
[System.Boolean]
3343
@@ -60,6 +70,13 @@
6070
Test-SqlDscIsDatabasePrincipal -ServerObject $serverInstance -DatabaseName 'MyDatabase' -Name 'MyPrincipal' -ExcludeApplicationRoles
6171
6272
Returns $true if the principal exist in the database and is not a application role, if not $false is returned.
73+
74+
.EXAMPLE
75+
$serverInstance = Connect-SqlDscDatabaseEngine
76+
Test-SqlDscIsDatabasePrincipal -ServerObject $serverInstance -DatabaseName 'MyDatabase' -Name 'MyPrincipal' -Refresh
77+
78+
Returns $true if the principal exist in the database, if not $false is returned.
79+
The database's principal collections are refreshed before testing.
6380
#>
6481
function Test-SqlDscIsDatabasePrincipal
6582
{
@@ -94,15 +111,45 @@ function Test-SqlDscIsDatabasePrincipal
94111

95112
[Parameter()]
96113
[System.Management.Automation.SwitchParameter]
97-
$ExcludeApplicationRoles
114+
$ExcludeApplicationRoles,
115+
116+
[Parameter()]
117+
[System.Management.Automation.SwitchParameter]
118+
$Refresh
98119
)
99120

100121
process
101122
{
102123
$principalExist = $false
103124

125+
if ($Refresh.IsPresent)
126+
{
127+
# Refresh the server's databases collection to ensure we have current data
128+
$ServerObject.Databases.Refresh()
129+
}
130+
104131
$sqlDatabaseObject = $ServerObject.Databases[$DatabaseName]
105132

133+
if ($Refresh.IsPresent -and $sqlDatabaseObject)
134+
{
135+
# Refresh the database object's collections to ensure we have current data
136+
# Only refresh collections that will be used based on exclude parameters
137+
if (-not $ExcludeRoles.IsPresent)
138+
{
139+
$sqlDatabaseObject.Roles.Refresh()
140+
}
141+
142+
if (-not $ExcludeUsers.IsPresent)
143+
{
144+
$sqlDatabaseObject.Users.Refresh()
145+
}
146+
147+
if (-not $ExcludeApplicationRoles.IsPresent)
148+
{
149+
$sqlDatabaseObject.ApplicationRoles.Refresh()
150+
}
151+
}
152+
106153
if (-not $sqlDatabaseObject)
107154
{
108155
$PSCmdlet.ThrowTerminatingError(
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')]
2+
param ()
3+
4+
BeforeDiscovery {
5+
try
6+
{
7+
if (-not (Get-Module -Name 'DscResource.Test'))
8+
{
9+
# Assumes dependencies have been resolved, so if this module is not available, run 'noop' task.
10+
if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable))
11+
{
12+
# Redirect all streams to $null, except the error stream (stream 2)
13+
& "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null
14+
}
15+
16+
# If the dependencies have not been resolved, this will throw an error.
17+
Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop'
18+
}
19+
}
20+
catch [System.IO.FileNotFoundException]
21+
{
22+
throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.'
23+
}
24+
}
25+
26+
BeforeAll {
27+
$script:moduleName = 'SqlServerDsc'
28+
29+
Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop'
30+
}
31+
32+
Describe 'Get-SqlDscDatabasePermission' -Tag @('Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') {
33+
BeforeAll {
34+
$script:mockInstanceName = 'DSCSQLTEST'
35+
36+
$mockSqlAdministratorUserName = 'SqlAdmin' # Using computer name as NetBIOS name throw exception.
37+
$mockSqlAdministratorPassword = ConvertTo-SecureString -String 'P@ssw0rd1' -AsPlainText -Force
38+
39+
$script:mockSqlAdminCredential = [System.Management.Automation.PSCredential]::new($mockSqlAdministratorUserName, $mockSqlAdministratorPassword)
40+
41+
$script:serverObject = Connect-SqlDscDatabaseEngine -InstanceName $script:mockInstanceName -Credential $script:mockSqlAdminCredential
42+
43+
# Create a test database for the integration tests
44+
$script:testDatabaseName = 'SqlDscDatabasePermissionTest_' + (Get-Random)
45+
$null = New-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction 'Stop'
46+
47+
# Create a test user in the database for permission testing
48+
$script:testUserName = 'SqlDscTestUser_' + (Get-Random)
49+
$testUserSql = "USE [$($script:testDatabaseName)]; CREATE USER [$($script:testUserName)] WITHOUT LOGIN;"
50+
Invoke-SqlDscQuery -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Query $testUserSql -Force -ErrorAction 'Stop'
51+
52+
# Grant some permissions to the test user for testing
53+
$grantPermissionSql = "USE [$($script:testDatabaseName)]; GRANT CONNECT, SELECT TO [$($script:testUserName)];"
54+
Invoke-SqlDscQuery -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Query $grantPermissionSql -Force -ErrorAction 'Stop'
55+
}
56+
57+
AfterAll {
58+
# Clean up test database (this will also remove the test user)
59+
$testDatabase = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -ErrorAction 'SilentlyContinue'
60+
if ($testDatabase)
61+
{
62+
$null = Remove-SqlDscDatabase -DatabaseObject $testDatabase -Force -ErrorAction 'Stop'
63+
}
64+
65+
Disconnect-SqlDscDatabaseEngine -ServerObject $script:serverObject
66+
}
67+
68+
Context 'When connecting to SQL Server instance' {
69+
Context 'When getting permissions for valid database principals' {
70+
It 'Should return permissions for dbo user in master database' {
71+
$result = Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName 'master' -Name 'dbo'
72+
73+
$result | Should -Not -BeNullOrEmpty
74+
$result | Should -BeOfType [Microsoft.SqlServer.Management.Smo.DatabasePermissionInfo]
75+
}
76+
77+
It 'Should return permissions for dbo user in master database using pipeline' {
78+
$result = $script:serverObject | Get-SqlDscDatabasePermission -DatabaseName 'master' -Name 'dbo'
79+
80+
$result | Should -Not -BeNullOrEmpty
81+
$result | Should -BeOfType [Microsoft.SqlServer.Management.Smo.DatabasePermissionInfo]
82+
}
83+
84+
It 'Should return permissions for public role in master database' {
85+
$result = Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName 'master' -Name 'public'
86+
87+
$result | Should -Not -BeNullOrEmpty
88+
$result | Should -BeOfType [Microsoft.SqlServer.Management.Smo.DatabasePermissionInfo]
89+
}
90+
91+
It 'Should return permissions for test user in test database' {
92+
$result = Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Name $script:testUserName
93+
94+
$result | Should -Not -BeNullOrEmpty
95+
$result | Should -BeOfType [Microsoft.SqlServer.Management.Smo.DatabasePermissionInfo]
96+
97+
# Verify that the Connect and Select permissions we granted are present
98+
$connectPermission = $result | Where-Object { $_.PermissionType.Connect -eq $true }
99+
$selectPermission = $result | Where-Object { $_.PermissionType.Select -eq $true }
100+
101+
$connectPermission | Should -Not -BeNullOrEmpty -Because 'Connect permission should have been granted to test user'
102+
$connectPermission.PermissionState | Should -Be 'Grant'
103+
104+
$selectPermission | Should -Not -BeNullOrEmpty -Because 'Select permission should have been granted to test user'
105+
$selectPermission.PermissionState | Should -Be 'Grant'
106+
}
107+
}
108+
109+
Context 'When getting permissions for invalid principals' {
110+
It 'Should throw error for non-existent database with ErrorAction Stop' {
111+
{ Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName 'NonExistentDatabase123' -Name 'dbo' -ErrorAction 'Stop' } |
112+
Should -Throw
113+
}
114+
115+
It 'Should return null for non-existent database with ErrorAction SilentlyContinue' {
116+
$result = Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName 'NonExistentDatabase123' -Name 'dbo' -ErrorAction 'SilentlyContinue'
117+
118+
$result | Should -BeNullOrEmpty
119+
}
120+
121+
It 'Should throw error for non-existent principal with ErrorAction Stop' {
122+
{ Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName 'master' -Name 'NonExistentUser123' -ErrorAction 'Stop' } |
123+
Should -Throw
124+
}
125+
126+
It 'Should return null for non-existent principal with ErrorAction SilentlyContinue' {
127+
$result = Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName 'master' -Name 'NonExistentUser123' -ErrorAction 'SilentlyContinue'
128+
129+
$result | Should -BeNullOrEmpty
130+
}
131+
}
132+
133+
Context 'When verifying permission properties' {
134+
BeforeAll {
135+
# Get permissions for a known principal that should have permissions
136+
$script:testPermissions = Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Name $script:testUserName
137+
}
138+
139+
It 'Should return DatabasePermissionInfo objects with PermissionState property' {
140+
$script:testPermissions | Should -Not -BeNullOrEmpty
141+
142+
foreach ($permission in $script:testPermissions) {
143+
$permission.PermissionState | Should -BeIn @('Grant', 'Deny', 'GrantWithGrant')
144+
}
145+
}
146+
147+
It 'Should return DatabasePermissionInfo objects with PermissionType property' {
148+
$script:testPermissions | Should -Not -BeNullOrEmpty
149+
150+
foreach ($permission in $script:testPermissions) {
151+
$permission.PermissionType | Should -Not -BeNullOrEmpty
152+
$permission.PermissionType | Should -BeOfType [Microsoft.SqlServer.Management.Smo.DatabasePermissionSet]
153+
}
154+
}
155+
156+
It 'Should return DatabasePermissionInfo objects with Grantee property' {
157+
$script:testPermissions | Should -Not -BeNullOrEmpty
158+
159+
foreach ($permission in $script:testPermissions) {
160+
$permission.Grantee | Should -Be $script:testUserName
161+
}
162+
}
163+
}
164+
165+
Context 'When working with built-in database roles' {
166+
It 'Should return permissions for db_datareader role' {
167+
# Note: The command excludes fixed roles by default, so this should return null or empty
168+
$result = Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Name 'db_datareader' -ErrorAction 'SilentlyContinue'
169+
170+
# Fixed roles are excluded by default, so result should be null
171+
$result | Should -BeNullOrEmpty
172+
}
173+
174+
It 'Should work with non-fixed database roles when they exist' {
175+
# Create a custom database role for testing
176+
$customRoleName = 'TestRole_' + (Get-Random)
177+
$createRoleSql = "USE [$($script:testDatabaseName)]; CREATE ROLE [$customRoleName];"
178+
Invoke-SqlDscQuery -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Query $createRoleSql -Force -ErrorAction 'Stop'
179+
180+
try
181+
{
182+
# Grant a permission to the custom role
183+
$grantRolePermissionSql = "USE [$($script:testDatabaseName)]; GRANT CONNECT TO [$customRoleName];"
184+
Invoke-SqlDscQuery -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Query $grantRolePermissionSql -Force -ErrorAction 'Stop'
185+
186+
# Test getting permissions for the custom role
187+
$result = Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Name $customRoleName -Refresh
188+
189+
$result | Should -Not -BeNullOrEmpty
190+
$result | Should -BeOfType [Microsoft.SqlServer.Management.Smo.DatabasePermissionInfo]
191+
192+
# Verify the Connect permission we granted is present
193+
$connectPermission = $result | Where-Object { $_.PermissionType.Connect -eq $true }
194+
$connectPermission | Should -Not -BeNullOrEmpty
195+
$connectPermission.PermissionState | Should -Be 'Grant'
196+
}
197+
finally
198+
{
199+
# Clean up the custom role
200+
$dropRoleSql = "USE [$($script:testDatabaseName)]; DROP ROLE [$customRoleName];"
201+
Invoke-SqlDscQuery -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Query $dropRoleSql -Force -ErrorAction 'SilentlyContinue'
202+
}
203+
}
204+
}
205+
}
206+
}

tests/Integration/Commands/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Get-SqlDscDatabase | 2 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTES
8181
New-SqlDscDatabase | 2 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | Test databases
8282
Set-SqlDscDatabase | 2 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | -
8383
Test-SqlDscDatabase | 2 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | -
84+
Get-SqlDscDatabasePermission | 2 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | Test database, Test user
8485
Invoke-SqlDscQuery | 2 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | Test database and table
8586
ConvertTo-SqlDscDatabasePermission | 2 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | -
8687
Set-SqlDscDatabasePermission | 2 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | -

tests/Unit/Public/Get-SqlDscDatabasePermission.Tests.ps1

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,5 +225,17 @@ Describe 'Get-SqlDscDatabasePermission' -Tag 'Public' {
225225
$mockResult[1].PermissionType.Update | Should -BeTrue
226226
}
227227
}
228+
229+
Context 'When using the Refresh parameter' {
230+
It 'Should pass the Refresh parameter to Test-SqlDscIsDatabasePrincipal' {
231+
Mock -CommandName Test-SqlDscIsDatabasePrincipal -MockWith {
232+
return $true
233+
} -ParameterFilter { $Refresh -eq $true }
234+
235+
$mockResult = Get-SqlDscDatabasePermission -ServerObject $mockServerObject -DatabaseName 'AdventureWorks' -Name 'Zebes\SamusAran' -Refresh -ErrorAction 'Stop'
236+
237+
Should -Invoke -CommandName Test-SqlDscIsDatabasePrincipal -ParameterFilter { $Refresh -eq $true } -Exactly -Times 1 -Scope It
238+
}
239+
}
228240
}
229241
}

0 commit comments

Comments
 (0)