Skip to content
Merged
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

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

### Changed

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

### Fixed

- `Add-SqlDscTraceFlag` and `Remove-SqlDscTraceFlag`
Expand Down
1 change: 1 addition & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ stages:
'tests/Integration/Commands/New-SqlDscDatabase.Integration.Tests.ps1'
'tests/Integration/Commands/Set-SqlDscDatabase.Integration.Tests.ps1'
'tests/Integration/Commands/Test-SqlDscDatabase.Integration.Tests.ps1'
'tests/Integration/Commands/Get-SqlDscDatabasePermission.Integration.Tests.ps1'
'tests/Integration/Commands/Invoke-SqlDscQuery.Integration.Tests.ps1'
'tests/Integration/Commands/Set-SqlDscDatabasePermission.Integration.Tests.ps1'
'tests/Integration/Commands/ConvertTo-SqlDscDatabasePermission.Integration.Tests.ps1'
Expand Down
26 changes: 25 additions & 1 deletion source/Public/Get-SqlDscDatabasePermission.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
Specifies the name of the database principal for which the permissions are
returned.

.PARAMETER Refresh
Specifies that the database's principal collections (Users, Roles, and
ApplicationRoles) should be refreshed before testing if the principal exists.
This is helpful when principals could have been modified outside of the
**ServerObject**, for example through T-SQL. But on databases with a large
amount of principals it might be better to make sure the **ServerObject**
is recent enough.

.OUTPUTS
[Microsoft.SqlServer.Management.Smo.DatabasePermissionInfo[]]

Expand All @@ -24,6 +32,13 @@

Get the permissions for the principal 'MyPrincipal'.

.EXAMPLE
$serverInstance = Connect-SqlDscDatabaseEngine
Get-SqlDscDatabasePermission -ServerObject $serverInstance -DatabaseName 'MyDatabase' -Name 'MyPrincipal' -Refresh

Get the permissions for the principal 'MyPrincipal'. The database's principal
collections are refreshed before testing if the principal exists.

.NOTES
This command excludes fixed roles like _db_datareader_ by default, and will
always return `$null` if a fixed role is specified as **Name**.
Expand Down Expand Up @@ -52,7 +67,11 @@ function Get-SqlDscDatabasePermission

[Parameter(Mandatory = $true)]
[System.String]
$Name
$Name,

[Parameter()]
[System.Management.Automation.SwitchParameter]
$Refresh
)

# cSpell: ignore GSDDP
Expand All @@ -76,6 +95,11 @@ function Get-SqlDscDatabasePermission
ExcludeFixedRoles = $true
}

if ($Refresh.IsPresent)
{
$testSqlDscIsDatabasePrincipalParameters.Refresh = $true
}

$isDatabasePrincipal = Test-SqlDscIsDatabasePrincipal @testSqlDscIsDatabasePrincipalParameters

if ($isDatabasePrincipal)
Expand Down
49 changes: 48 additions & 1 deletion source/Public/Test-SqlDscIsDatabasePrincipal.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@
Specifies that fixed application roles should not be evaluated for the
specified name.

.PARAMETER Refresh
Specifies that the database's principal collections (Users, Roles, and
ApplicationRoles) should be refreshed before testing if the principal exists.
This is helpful when principals could have been modified outside of the
**ServerObject**, for example through T-SQL. But on databases with a large
amount of principals it might be better to make sure the **ServerObject**
is recent enough. When exclude parameters are specified (e.g., **ExcludeUsers**,
**ExcludeRoles**, **ExcludeApplicationRoles**), only the collections that will
be used are refreshed to improve performance.

.OUTPUTS
[System.Boolean]

Expand Down Expand Up @@ -60,6 +70,13 @@
Test-SqlDscIsDatabasePrincipal -ServerObject $serverInstance -DatabaseName 'MyDatabase' -Name 'MyPrincipal' -ExcludeApplicationRoles

Returns $true if the principal exist in the database and is not a application role, if not $false is returned.

.EXAMPLE
$serverInstance = Connect-SqlDscDatabaseEngine
Test-SqlDscIsDatabasePrincipal -ServerObject $serverInstance -DatabaseName 'MyDatabase' -Name 'MyPrincipal' -Refresh

Returns $true if the principal exist in the database, if not $false is returned.
The database's principal collections are refreshed before testing.
#>
function Test-SqlDscIsDatabasePrincipal
{
Expand Down Expand Up @@ -94,15 +111,45 @@ function Test-SqlDscIsDatabasePrincipal

[Parameter()]
[System.Management.Automation.SwitchParameter]
$ExcludeApplicationRoles
$ExcludeApplicationRoles,

[Parameter()]
[System.Management.Automation.SwitchParameter]
$Refresh
)

process
{
$principalExist = $false

if ($Refresh.IsPresent)
{
# Refresh the server's databases collection to ensure we have current data
$ServerObject.Databases.Refresh()
}

$sqlDatabaseObject = $ServerObject.Databases[$DatabaseName]

if ($Refresh.IsPresent -and $sqlDatabaseObject)
{
# Refresh the database object's collections to ensure we have current data
# Only refresh collections that will be used based on exclude parameters
if (-not $ExcludeRoles.IsPresent)
{
$sqlDatabaseObject.Roles.Refresh()
}

if (-not $ExcludeUsers.IsPresent)
{
$sqlDatabaseObject.Users.Refresh()
}

if (-not $ExcludeApplicationRoles.IsPresent)
{
$sqlDatabaseObject.ApplicationRoles.Refresh()
}
}

if (-not $sqlDatabaseObject)
{
$PSCmdlet.ThrowTerminatingError(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')]
param ()

BeforeDiscovery {
try
{
if (-not (Get-Module -Name 'DscResource.Test'))
{
# Assumes dependencies have been resolved, so if this module is not available, run 'noop' task.
if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable))
{
# Redirect all streams to $null, except the error stream (stream 2)
& "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null
}

# If the dependencies have not been resolved, this will throw an error.
Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop'
}
}
catch [System.IO.FileNotFoundException]
{
throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.'
}
}

BeforeAll {
$script:moduleName = 'SqlServerDsc'

Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop'
}

Describe 'Get-SqlDscDatabasePermission' -Tag @('Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') {
BeforeAll {
$script:mockInstanceName = 'DSCSQLTEST'

$mockSqlAdministratorUserName = 'SqlAdmin' # Using computer name as NetBIOS name throw exception.
$mockSqlAdministratorPassword = ConvertTo-SecureString -String 'P@ssw0rd1' -AsPlainText -Force

$script:mockSqlAdminCredential = [System.Management.Automation.PSCredential]::new($mockSqlAdministratorUserName, $mockSqlAdministratorPassword)

$script:serverObject = Connect-SqlDscDatabaseEngine -InstanceName $script:mockInstanceName -Credential $script:mockSqlAdminCredential

# Create a test database for the integration tests
$script:testDatabaseName = 'SqlDscDatabasePermissionTest_' + (Get-Random)
$null = New-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction 'Stop'

# Create a test user in the database for permission testing
$script:testUserName = 'SqlDscTestUser_' + (Get-Random)
$testUserSql = "USE [$($script:testDatabaseName)]; CREATE USER [$($script:testUserName)] WITHOUT LOGIN;"
Invoke-SqlDscQuery -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Query $testUserSql -Force -ErrorAction 'Stop'

# Grant some permissions to the test user for testing
$grantPermissionSql = "USE [$($script:testDatabaseName)]; GRANT CONNECT, SELECT TO [$($script:testUserName)];"
Invoke-SqlDscQuery -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Query $grantPermissionSql -Force -ErrorAction 'Stop'
}

AfterAll {
# Clean up test database (this will also remove the test user)
$testDatabase = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -ErrorAction 'SilentlyContinue'
if ($testDatabase)
{
$null = Remove-SqlDscDatabase -DatabaseObject $testDatabase -Force -ErrorAction 'Stop'
}

Disconnect-SqlDscDatabaseEngine -ServerObject $script:serverObject
}

Context 'When connecting to SQL Server instance' {
Context 'When getting permissions for valid database principals' {
It 'Should return permissions for dbo user in master database' {
$result = Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName 'master' -Name 'dbo'

$result | Should -Not -BeNullOrEmpty
$result | Should -BeOfType [Microsoft.SqlServer.Management.Smo.DatabasePermissionInfo]
}

It 'Should return permissions for dbo user in master database using pipeline' {
$result = $script:serverObject | Get-SqlDscDatabasePermission -DatabaseName 'master' -Name 'dbo'

$result | Should -Not -BeNullOrEmpty
$result | Should -BeOfType [Microsoft.SqlServer.Management.Smo.DatabasePermissionInfo]
}

It 'Should return permissions for public role in master database' {
$result = Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName 'master' -Name 'public'

$result | Should -Not -BeNullOrEmpty
$result | Should -BeOfType [Microsoft.SqlServer.Management.Smo.DatabasePermissionInfo]
}

It 'Should return permissions for test user in test database' {
$result = Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Name $script:testUserName

$result | Should -Not -BeNullOrEmpty
$result | Should -BeOfType [Microsoft.SqlServer.Management.Smo.DatabasePermissionInfo]

# Verify that the Connect and Select permissions we granted are present
$connectPermission = $result | Where-Object { $_.PermissionType.Connect -eq $true }
$selectPermission = $result | Where-Object { $_.PermissionType.Select -eq $true }

$connectPermission | Should -Not -BeNullOrEmpty -Because 'Connect permission should have been granted to test user'
$connectPermission.PermissionState | Should -Be 'Grant'

$selectPermission | Should -Not -BeNullOrEmpty -Because 'Select permission should have been granted to test user'
$selectPermission.PermissionState | Should -Be 'Grant'
}
}

Context 'When getting permissions for invalid principals' {
It 'Should throw error for non-existent database with ErrorAction Stop' {
{ Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName 'NonExistentDatabase123' -Name 'dbo' -ErrorAction 'Stop' } |
Should -Throw
}

It 'Should return null for non-existent database with ErrorAction SilentlyContinue' {
$result = Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName 'NonExistentDatabase123' -Name 'dbo' -ErrorAction 'SilentlyContinue'

$result | Should -BeNullOrEmpty
}

It 'Should throw error for non-existent principal with ErrorAction Stop' {
{ Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName 'master' -Name 'NonExistentUser123' -ErrorAction 'Stop' } |
Should -Throw
}

It 'Should return null for non-existent principal with ErrorAction SilentlyContinue' {
$result = Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName 'master' -Name 'NonExistentUser123' -ErrorAction 'SilentlyContinue'

$result | Should -BeNullOrEmpty
}
}

Context 'When verifying permission properties' {
BeforeAll {
# Get permissions for a known principal that should have permissions
$script:testPermissions = Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Name $script:testUserName
}

It 'Should return DatabasePermissionInfo objects with PermissionState property' {
$script:testPermissions | Should -Not -BeNullOrEmpty

foreach ($permission in $script:testPermissions) {
$permission.PermissionState | Should -BeIn @('Grant', 'Deny', 'GrantWithGrant')
}
}

It 'Should return DatabasePermissionInfo objects with PermissionType property' {
$script:testPermissions | Should -Not -BeNullOrEmpty

foreach ($permission in $script:testPermissions) {
$permission.PermissionType | Should -Not -BeNullOrEmpty
$permission.PermissionType | Should -BeOfType [Microsoft.SqlServer.Management.Smo.DatabasePermissionSet]
}
}

It 'Should return DatabasePermissionInfo objects with Grantee property' {
$script:testPermissions | Should -Not -BeNullOrEmpty

foreach ($permission in $script:testPermissions) {
$permission.Grantee | Should -Be $script:testUserName
}
}
}

Context 'When working with built-in database roles' {
It 'Should return permissions for db_datareader role' {
# Note: The command excludes fixed roles by default, so this should return null or empty
$result = Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Name 'db_datareader' -ErrorAction 'SilentlyContinue'

# Fixed roles are excluded by default, so result should be null
$result | Should -BeNullOrEmpty
}

It 'Should work with non-fixed database roles when they exist' {
# Create a custom database role for testing
$customRoleName = 'TestRole_' + (Get-Random)
$createRoleSql = "USE [$($script:testDatabaseName)]; CREATE ROLE [$customRoleName];"
Invoke-SqlDscQuery -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Query $createRoleSql -Force -ErrorAction 'Stop'

try
{
# Grant a permission to the custom role
$grantRolePermissionSql = "USE [$($script:testDatabaseName)]; GRANT CONNECT TO [$customRoleName];"
Invoke-SqlDscQuery -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Query $grantRolePermissionSql -Force -ErrorAction 'Stop'

# Test getting permissions for the custom role
$result = Get-SqlDscDatabasePermission -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Name $customRoleName -Refresh

$result | Should -Not -BeNullOrEmpty
$result | Should -BeOfType [Microsoft.SqlServer.Management.Smo.DatabasePermissionInfo]

# Verify the Connect permission we granted is present
$connectPermission = $result | Where-Object { $_.PermissionType.Connect -eq $true }
$connectPermission | Should -Not -BeNullOrEmpty
$connectPermission.PermissionState | Should -Be 'Grant'
}
finally
{
# Clean up the custom role
$dropRoleSql = "USE [$($script:testDatabaseName)]; DROP ROLE [$customRoleName];"
Invoke-SqlDscQuery -ServerObject $script:serverObject -DatabaseName $script:testDatabaseName -Query $dropRoleSql -Force -ErrorAction 'SilentlyContinue'
}
}
}
}
}
1 change: 1 addition & 0 deletions tests/Integration/Commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Get-SqlDscDatabase | 2 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTES
New-SqlDscDatabase | 2 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | Test databases
Set-SqlDscDatabase | 2 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | -
Test-SqlDscDatabase | 2 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | -
Get-SqlDscDatabasePermission | 2 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | Test database, Test user
Invoke-SqlDscQuery | 2 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | Test database and table
ConvertTo-SqlDscDatabasePermission | 2 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | -
Set-SqlDscDatabasePermission | 2 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | -
Expand Down
12 changes: 12 additions & 0 deletions tests/Unit/Public/Get-SqlDscDatabasePermission.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -225,5 +225,17 @@ Describe 'Get-SqlDscDatabasePermission' -Tag 'Public' {
$mockResult[1].PermissionType.Update | Should -BeTrue
}
}

Context 'When using the Refresh parameter' {
It 'Should pass the Refresh parameter to Test-SqlDscIsDatabasePrincipal' {
Mock -CommandName Test-SqlDscIsDatabasePrincipal -MockWith {
return $true
} -ParameterFilter { $Refresh -eq $true }

$mockResult = Get-SqlDscDatabasePermission -ServerObject $mockServerObject -DatabaseName 'AdventureWorks' -Name 'Zebes\SamusAran' -Refresh -ErrorAction 'Stop'

Should -Invoke -CommandName Test-SqlDscIsDatabasePrincipal -ParameterFilter { $Refresh -eq $true } -Exactly -Times 1 -Scope It
}
}
}
}
Loading
Loading