diff --git a/DSCResources/MSFT_xADCommon/MSFT_xADCommon.psm1 b/DSCResources/MSFT_xADCommon/MSFT_xADCommon.psm1 index 489ba02b2..afc3dba01 100644 --- a/DSCResources/MSFT_xADCommon/MSFT_xADCommon.psm1 +++ b/DSCResources/MSFT_xADCommon/MSFT_xADCommon.psm1 @@ -10,6 +10,7 @@ data localizedString IncludeAndExcludeAreEmptyError = The '{0}' and '{1}' parameters are either both null or empty. At least one member must be specified in one of these parameters. ModeConversionError = Converted mode {0} is not a {1}. RecycleBinRestoreFailed = Restoring {0} ({1}) from the recycle bin failed. Error message: {2}. + EmptyDomainError = No domain name retrieved for group member {0} in group {1}. CheckingMembers = Checking for '{0}' members. MembershipCountMismatch = Membership count is not correct. Expected '{0}' members, actual '{1}' members. @@ -22,6 +23,7 @@ data localizedString FindInRecycleBin = Finding objects in the recycle bin matching the filter {0}. FoundRestoreTargetInRecycleBin = Found object {0} ({1}) in the recycle bin as {2}. Attempting to restore the object. RecycleBinRestoreSuccessful = Successfully restored object {0} ({1}) from the recycle bin. + AddingGroupMember = Adding member '{0}' from domain '{1}' to AD group '{2}'. '@ } @@ -55,7 +57,8 @@ function Assert-Module } #end function Assert-Module # Internal function to test whether computer is a member of a domain -function Test-DomainMember { +function Test-DomainMember +{ [CmdletBinding()] [OutputType([System.Boolean])] param ( ) @@ -65,7 +68,8 @@ function Test-DomainMember { # Internal function to get the domain name of the computer -function Get-DomainName { +function Get-DomainName +{ [CmdletBinding()] [OutputType([System.String])] param ( ) @@ -74,10 +78,11 @@ function Get-DomainName { } # function Get-DomainName # Internal function to build domain FQDN -function Resolve-DomainFQDN { +function Resolve-DomainFQDN +{ [CmdletBinding()] param ( - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [OutputType([System.String])] [System.String] $DomainName, @@ -99,7 +104,7 @@ function Test-ADDomain [OutputType([System.Boolean])] param ( - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [System.String] $DomainName, [Parameter()] @@ -151,7 +156,7 @@ function Get-ADObjectParentDN [OutputType([System.String])] param ( - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [System.String] $DN ) @@ -169,18 +174,22 @@ function Assert-MemberParameters [CmdletBinding()] param ( + [Parameter()] [ValidateNotNull()] [System.String[]] $Members, + [Parameter()] [ValidateNotNull()] [System.String[]] $MembersToInclude, + [Parameter()] [ValidateNotNull()] [System.String[]] $MembersToExclude, + [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $ModuleName = 'xActiveDirectory' @@ -244,7 +253,9 @@ function Remove-DuplicateMembers [OutputType([System.String[]])] param ( - [System.String[]] $Members + [Parameter()] + [System.String[]] + $Members ) Set-StrictMode -Version Latest @@ -289,21 +300,25 @@ function Test-Members param ( ## Existing array members + [Parameter()] [AllowNull()] [System.String[]] $ExistingMembers, ## Explicit array members + [Parameter()] [AllowNull()] [System.String[]] $Members, ## Compulsory array members + [Parameter()] [AllowNull()] [System.String[]] $MembersToInclude, ## Excluded array members + [Parameter()] [AllowNull()] [System.String[]] $MembersToExclude @@ -380,12 +395,12 @@ function ConvertTo-TimeSpan [OutputType([System.TimeSpan])] param ( - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.UInt32] $TimeSpan, - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [ValidateSet('Seconds','Minutes','Hours','Days')] [System.String] $TimeSpanType @@ -419,12 +434,12 @@ function ConvertFrom-TimeSpan [OutputType([System.Int32])] param ( - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.TimeSpan] $TimeSpan, - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [ValidateSet('Seconds','Minutes','Hours','Days')] [System.String] $TimeSpanType @@ -466,7 +481,7 @@ function Get-ADCommonParameters [OutputType([System.Collections.Hashtable])] param ( - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [Alias('UserName','GroupName','ComputerName')] [System.String] @@ -542,12 +557,12 @@ function ThrowInvalidOperationError [CmdletBinding()] param ( - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ErrorId, - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ErrorMessage @@ -564,12 +579,12 @@ function ThrowInvalidArgumentError [CmdletBinding()] param ( - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ErrorId, - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ErrorMessage @@ -589,10 +604,10 @@ function Test-ADReplicationSite [OutputType([System.Boolean])] param ( - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [System.String] $SiteName, - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [System.String] $DomainName, [Parameter()] @@ -601,7 +616,7 @@ function Test-ADReplicationSite ) Write-Verbose -Message ($localizedString.CheckingSite -f $SiteName); - + $existingDC = "$((Get-ADDomainController -Discover -DomainName $DomainName -ForceDiscover).HostName)"; try @@ -635,6 +650,7 @@ function ConvertTo-DeploymentForestMode [System.Nullable``1[Microsoft.ActiveDirectory.Management.ADForestMode]] $Mode, + [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $ModuleName = 'xActiveDirectory' @@ -646,7 +662,7 @@ function ConvertTo-DeploymentForestMode { $convertedMode = $Mode -as [Microsoft.DirectoryServices.Deployment.Types.ForestMode] } - + if ($PSCmdlet.ParameterSetName -eq 'ById') { $convertedMode = $ModeId -as [Microsoft.DirectoryServices.Deployment.Types.ForestMode] @@ -679,18 +695,19 @@ function ConvertTo-DeploymentDomainMode [System.Nullable``1[Microsoft.ActiveDirectory.Management.ADDomainMode]] $Mode, + [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $ModuleName = 'xActiveDirectory' ) - + $convertedMode = $null if ($PSCmdlet.ParameterSetName -eq 'ByName' -and $Mode) { $convertedMode = $Mode -as [Microsoft.DirectoryServices.Deployment.Types.DomainMode] } - + if ($PSCmdlet.ParameterSetName -eq 'ById') { $convertedMode = $ModeId -as [Microsoft.DirectoryServices.Deployment.Types.DomainMode] @@ -709,7 +726,7 @@ function Restore-ADCommonObject [CmdletBinding()] param ( - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [Alias('UserName','GroupName','ComputerName')] [System.String] @@ -772,3 +789,105 @@ function Restore-ADCommonObject return $restoredObject } + +<# + .SYNOPSIS + Author: Robert D. Biddle (https://github.com/RobBiddle) + Created: December.20.2017 + + .DESCRIPTION + Takes an Active Directory DistinguishedName as input, returns the domain FQDN + + .EXAMPLE + Get-ADDomainNameFromDistinguishedName -DistinguishedName 'CN=ExampleObject,OU=ExampleOU,DC=example,DC=com' +#> +function Get-ADDomainNameFromDistinguishedName +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $DistinguishedName + ) + + if ($DistinguishedName -notlike '*DC=*') + { + return + } + + $splitDistinguishedName = ($DistinguishedName -split 'DC=') + $splitDistinguishedNameParts = $splitDistinguishedName[1..$splitDistinguishedName.Length] + $domainFqdn = "" + foreach ($part in $splitDistinguishedNameParts) + { + $domainFqdn += "DC=$part" + } + + $domainName = $domainFqdn -replace 'DC=', '' -replace ',', '.' + return $domainName + +} #end function Get-ADDomainNameFromDistinguishedName + +<# + .SYNOPSIS + Add group member from current or different domain + + .NOTES + Author original code: Robert D. Biddle (https://github.com/RobBiddle) + Author refactored code: Jan-Hendrik Peters (https://github.com/nyanhp) +#> +function Add-ADCommonGroupMember +{ + [CmdletBinding()] + param + ( + [Parameter()] + [string[]] + $Members, + + [Parameter()] + [hashtable] + $Parameters, + + [Parameter()] + [switch] + $MembersInMultipleDomains + ) + + Assert-Module -ModuleName ActiveDirectory + + if ($MembersInMultipleDomains.IsPresent) + { + foreach($member in $Members) + { + $memberDomain = Get-ADDomainNameFromDistinguishedName -DistinguishedName $member + + if (-not $memberDomain) + { + ThrowInvalidArgumentError -ErrorId "$($member)_EmptyDomainError" -ErrorMessage ($localizedString.EmptyDomainError -f $member, $Parameters.GroupName) + } + + Write-Verbose -Message ($localizedString.AddingGroupMember -f $member, $memberDomain, $Parameters.GroupName) + $memberObjectClass = (Get-ADObject -Identity $member -Server $memberDomain -Properties ObjectClass).ObjectClass + if ($memberObjectClass -eq 'computer') + { + $memberObject = Get-ADComputer -Identity $member -Server $memberDomain + } + elseif ($memberObjectClass -eq 'group') + { + $memberObject = Get-ADGroup -Identity $member -Server $memberDomain + } + elseif ($memberObjectClass -eq 'user') + { + $memberObject = Get-ADUser -Identity $member -Server $memberDomain + } + + Add-ADGroupMember @Parameters -Members $memberObject + } + } + else + { + Add-ADGroupMember @Parameters -Members $Members + } +} diff --git a/DSCResources/MSFT_xADGroup/MSFT_xADGroup.psm1 b/DSCResources/MSFT_xADGroup/MSFT_xADGroup.psm1 index b10430ab0..4e00a693d 100644 --- a/DSCResources/MSFT_xADGroup/MSFT_xADGroup.psm1 +++ b/DSCResources/MSFT_xADGroup/MSFT_xADGroup.psm1 @@ -23,6 +23,7 @@ data LocalizedData GroupNotFound = AD Group '{0}' was not found NotDesiredPropertyState = AD Group '{0}' is not correct. Expected '{1}', actual '{2}' UpdatingGroupProperty = Updating AD Group property '{0}' to '{1}' + GroupMembershipMultipleDomains = Group membership objects are in '{0}' different AD Domains. '@ } @@ -402,6 +403,22 @@ function Set-TargetResource try { + if ($MembershipAttribute -eq 'DistinguishedName') + { + $AllMembers = $Members + $MembersToInclude + $MembersToExclude + $GroupMemberDomains = @(); + foreach($member in $AllMembers) + { + $GroupMemberDomains += Get-ADDomainNameFromDistinguishedName -DistinguishedName $member + } + $GroupMemberDomainCount = ($GroupMemberDomains | Select-Object -Unique).count + if( $GroupMemberDomainCount -gt 1 -or ($GroupMemberDomains -ine (Get-DomainName)).Count -gt 0 ) + { + Write-Verbose -Message ($LocalizedData.GroupMembershipMultipleDomains -f $GroupMemberDomainCount); + $MembersInMultipleDomains = $true + } + } + $adGroup = Get-ADGroup @adGroupParams -Property Name,GroupScope,GroupCategory,DistinguishedName,Description,DisplayName,ManagedBy,Info if ($Ensure -eq 'Present') @@ -471,13 +488,13 @@ function Set-TargetResource Remove-ADGroupMember @adGroupParams -Members $adGroupMembers -Confirm:$false } Write-Verbose -Message ($LocalizedData.AddingGroupMembers -f $Members.Count, $GroupName) - Add-ADGroupMember @adGroupParams -Members $Members + Add-ADCommonGroupMember -Parameter $adGroupParams -Members $Members -MembersInMultipleDomains:$MembersInMultipleDomains } if ($PSBoundParameters.ContainsKey('MembersToInclude') -and -not [system.string]::IsNullOrEmpty($MembersToInclude)) { $MembersToInclude = Remove-DuplicateMembers -Members $MembersToInclude Write-Verbose -Message ($LocalizedData.AddingGroupMembers -f $MembersToInclude.Count, $GroupName) - Add-ADGroupMember @adGroupParams -Members $MembersToInclude + Add-ADCommonGroupMember -Parameter $adGroupParams -Members $MembersToInclude -MembersInMultipleDomains:$MembersInMultipleDomains } if ($PSBoundParameters.ContainsKey('MembersToExclude') -and -not [system.string]::IsNullOrEmpty($MembersToExclude)) { @@ -518,7 +535,7 @@ function Set-TargetResource { $adGroupParams['Path'] = $Path } - + <# Create group Try to restore account first if it exists @@ -554,13 +571,13 @@ function Set-TargetResource { $Members = Remove-DuplicateMembers -Members $Members Write-Verbose -Message ($LocalizedData.AddingGroupMembers -f $Members.Count, $GroupName) - Add-ADGroupMember @adGroupParams -Members $Members + Add-ADCommonGroupMember -Parameter $adGroupParams -Members $Members -MembersInMultipleDomains:$MembersInMultipleDomains } elseif ($PSBoundParameters.ContainsKey('MembersToInclude') -and -not [system.string]::IsNullOrEmpty($MembersToInclude)) { $MembersToInclude = Remove-DuplicateMembers -Members $MembersToInclude Write-Verbose -Message ($LocalizedData.AddingGroupMembers -f $MembersToInclude.Count, $GroupName) - Add-ADGroupMember @adGroupParams -Members $MembersToInclude + Add-ADCommonGroupMember -Parameter $adGroupParams -Members $MembersToInclude -MembersInMultipleDomains:$MembersInMultipleDomains } } diff --git a/Examples/Resources/xADGroup/1-NewGroup.ps1 b/Examples/Resources/xADGroup/1-NewGroup.ps1 new file mode 100644 index 000000000..3bbbc23a7 --- /dev/null +++ b/Examples/Resources/xADGroup/1-NewGroup.ps1 @@ -0,0 +1,17 @@ +<# +.EXAMPLE + This example creates a new domain-local group in contoso +#> +configuration Example +{ + Import-DscResource -ModuleName xActiveDirectory + + node localhost + { + xADGroup dl1 + { + GroupName = 'DL_APP_1' + GroupScope = 'DomainLocal' + } + } +} diff --git a/Examples/Resources/xADGroup/2-NewGroupWithMembers.ps1 b/Examples/Resources/xADGroup/2-NewGroupWithMembers.ps1 new file mode 100644 index 000000000..d798a8e26 --- /dev/null +++ b/Examples/Resources/xADGroup/2-NewGroupWithMembers.ps1 @@ -0,0 +1,18 @@ +<# +.EXAMPLE + This example creates a new domain-local group in contoso with three members. +#> +configuration Example +{ + Import-DscResource -ModuleName xActiveDirectory + + node localhost + { + xADGroup dl1 + { + GroupName = 'DL_APP_1' + GroupScope = 'DomainLocal' + Members = 'john','jim','sally' + } + } +} diff --git a/Examples/Resources/xADGroup/3-NewGroupMultidomainMembers.ps1 b/Examples/Resources/xADGroup/3-NewGroupMultidomainMembers.ps1 new file mode 100644 index 000000000..3404d909a --- /dev/null +++ b/Examples/Resources/xADGroup/3-NewGroupMultidomainMembers.ps1 @@ -0,0 +1,19 @@ +<# +.EXAMPLE + This example creates a new domain-local group in contoso with three members in different domains. +#> +configuration Example +{ + Import-DscResource -ModuleName xActiveDirectory + + node localhost + { + xADGroup dl1 + { + GroupName = 'DL_APP_1' + GroupScope = 'DomainLocal' + MembershipAttribute = 'DistinguishedName' + Members = 'CN=john,OU=Accounts,DC=contoso,DC=com','CN=jim,OU=Accounts,DC=subdomain,DC=contoso,DC=com','CN=sally,OU=Accounts,DC=anothersub,DC=contoso,DC=com' + } + } +} diff --git a/README.md b/README.md index 23ab8b7df..535535bd8 100644 --- a/README.md +++ b/README.md @@ -165,15 +165,18 @@ The xADGroup DSC resource will manage groups within Active Directory. * If not specified, no group membership changes are made. * If specified, all undefined group members will be removed the AD group. * This property cannot be specified with either 'MembersToInclude' or 'MembersToExclude'. + * To use other domain's members, specify the distinguished name of the object. * **`[String[]]` MembersToInclude** _(Write)_: Specifies AD objects that must be in the group. * If not specified, no group membership changes are made. * If specified, only the specified members are added to the group. * If specified, no users are removed from the group using this parameter. + * To use other domain's members, specify the distinguished name of the object. * This property cannot be specified with the 'Members' parameter. * **`[String[]]` MembersToExclude** _(Write)_: Specifies AD objects that _must not_ be in the group. * If not specified, no group membership changes are made. * If specified, only those specified are removed from the group. * If specified, no users are added to the group using this parameter. + * To use other domain's members, specify the distinguished name of the object. * This property cannot be specified with the 'Members' parameter. * **`[String]` MembershipAttribute** _(Write)_: Defines the AD object attribute that is used to determine group membership. * Valid values are 'SamAccountName', 'DistinguishedName', 'ObjectGUID' and 'SID'. @@ -356,6 +359,7 @@ The xADForestProperties DSC resource will manage User Principal Name (UPN) suffi * Added parameter to xADDomainController to support InstallationMediaPath ([issue #108](https://github.com/PowerShell/xActiveDirectory/issues/108)). * Updated xADDomainController schema to be standard and provide Descriptions. +* Updated xADGroup to support group membership from multiple domains ([issue #152](https://github.com/PowerShell/xActiveDirectory/issues/152)). [Robert Biddle (@robbiddle)](https://github.com/RobBiddle) and [Jan-Hendrik Peters (@nyanhp)](https://github.com/nyanhp) ### 2.23.0.0 diff --git a/Tests/Unit/MSFT_xADCommon.Tests.ps1 b/Tests/Unit/MSFT_xADCommon.Tests.ps1 index ec31ca168..6c6e0e2bd 100644 --- a/Tests/Unit/MSFT_xADCommon.Tests.ps1 +++ b/Tests/Unit/MSFT_xADCommon.Tests.ps1 @@ -569,7 +569,7 @@ try It 'Throws no exception when a null value is passed' { { ConvertTo-DeploymentForestMode -Mode $null } | Should Not Throw } - + It 'Throws no exception when an invalid mode id is selected' { { ConvertTo-DeploymentForestMode -ModeId 666 } | Should Not Throw } @@ -609,7 +609,7 @@ try It 'Throws no exception when a null value is passed' { { ConvertTo-DeploymentDomainMode -Mode $null } | Should Not Throw } - + It 'Throws no exception when an invalid mode id is selected' { { ConvertTo-DeploymentDomainMode -ModeId 666 } | Should Not Throw } @@ -639,7 +639,7 @@ try ObjectClass = 'user' ObjectGUID = 'd3c8b8c1-c42b-4533-af7d-3aa73ecd2216' } - + function Restore-ADObject { } $getAdCommonParameterReturnValue = @{Identity = 'something'} @@ -675,7 +675,7 @@ try {Restore-ADCommonObject -Identity $restoreIdentity -ObjectClass $restoreObjectClass} | Should -Throw -ExceptionType ([System.InvalidOperationException]) } } - + Context 'When there are no objects in the recycle bin' { Mock -CommandName Get-ADObject Mock -CommandName Get-ADCommonParameters -MockWith { return $getAdCommonParameterReturnValue} @@ -683,7 +683,7 @@ try It 'Should return $null' { Restore-ADCommonObject -Identity $restoreIdentity -ObjectClass $restoreObjectClass | Should -Be $null - } + } It 'Should not call Restore-ADObject' { Restore-ADCommonObject -Identity $restoreIdentity -ObjectClass $restoreObjectClass @@ -693,6 +693,170 @@ try } #endregion + #region Get-ADDomainNameFromDistinguishedName + Describe "$($Global:DSCResourceName)\Get-ADDomainNameFromDistinguishedName" { + $validDistinguishedNames = @( + @{ + DN = 'CN=group1,OU=Group,OU=Wacken,DC=contoso,DC=com' + Domain = 'contoso.com' + } + @{ + DN = 'CN=group1,OU=Group,OU=Wacken,DC=sub,DC=contoso,DC=com' + Domain = 'sub.contoso.com' + } + @{ + DN = 'CN=group1,OU=Group,OU=Wacken,DC=child,DC=sub,DC=contoso,DC=com' + Domain = 'child.sub.contoso.com' + } + ) + $invalidDistinguishedNames = @( + 'Group1' + 'contoso\group1' + 'user1@contoso.com' + ) + + Context 'The distinguished name is valid' { + foreach ($name in $validDistinguishedNames) + { + It "Should match domain $($name.Domain)" { + Get-ADDomainNameFromDistinguishedName -DistinguishedName $name.Dn | Should -Be $name.Domain + } + } + } + + Context 'The distinguished name is invalid' { + foreach ($name in $invalidDistinguishedNames) + { + It "Should return `$null for $name" { + Get-ADDomainNameFromDistinguishedName -DistinguishedName $name | Should -Be $null + } + } + } + } + #endregion + + #region Add-AdCommonGroupMember + Describe "$($Global:DSCResourceName)\Add-ADCommonGroupMember" { + Mock -CommandName Assert-Module -ParameterFilter { $ModuleName -eq 'ActiveDirectory' } + + $memberData = @( + [pscustomobject]@{ + Name = 'CN=Account1,DC=contoso,DC=com' + Domain = 'contoso.com' + } + [pscustomobject]@{ + Name = 'CN=Group1,DC=contoso,DC=com' + Domain = 'contoso.com' + } + [pscustomobject]@{ + Name = 'CN=Computer1,DC=contoso,DC=com' + Domain = 'contoso.com' + } + [pscustomobject]@{ + Name = 'CN=Account1,DC=a,DC=contoso,DC=com' + Domain = 'a.contoso.com' + } + [pscustomobject]@{ + Name = 'CN=Group1,DC=a,DC=contoso,DC=com' + Domain = 'a.contoso.com' + } + [pscustomobject]@{ + Name = 'CN=Computer1,DC=a,DC=contoso,DC=com' + Domain = 'a.contoso.com' + } + [pscustomobject]@{ + Name = 'CN=Account1,DC=b,DC=contoso,DC=com' + Domain = 'b.contoso.com' + } + [pscustomobject]@{ + Name = 'CN=Group1,DC=b,DC=contoso,DC=com' + Domain = 'b.contoso.com' + } + [pscustomobject]@{ + Name = 'CN=Computer1,DC=b,DC=contoso,DC=com' + Domain = 'b.contoso.com' + } + ) + + $invalidMemberData = @( + 'contoso.com\group1' + 'user1@contoso.com' + 'computer1.contoso.com' + ) + + $fakeParameters = @{ + Identity = 'SomeGroup' + } + + Context 'When all members are in the same domain' { + Mock -CommandName Add-ADGroupMember + $groupCount = 0 + foreach ($domainGroup in ($memberData | Group-Object -Property Domain)) + { + $groupCount ++ + It 'Should not throw an error when calling Add-ADCommonGroupMember' { + Add-ADCommonGroupMember -Members $domainGroup.Group.Name -Parameters $fakeParameters + } + } + + It "Should have called Add-ADGroupMember $groupCount times" { + Assert-MockCalled -CommandName Add-ADGroupMember -Exactly -Times $groupCount + } + } + + Context 'When members are in different domains' { + Mock -CommandName Add-ADGroupMember + Mock -CommandName Get-ADObject -MockWith { + param ( + [Parameter()] + [string] + $Identity, + + [Parameter()] + [string] + $Server, + + [Parameter()] + [string[]] + $Properties + ) + + $objectClass = switch ($Identity) + { + {$Identity -match 'Group'} { 'group' } + {$Identity -match 'Account'} { 'user' } + {$Identity -match 'Computer'} { 'computer' } + } + + return ([PSCustomObject]@{ + objectClass = $objectClass + }) + } + # Mocks should return something that is used with Add-ADGroupMember + Mock -CommandName Get-ADComputer -MockWith { return 'placeholder' } + Mock -CommandName Get-ADGroup -MockWith { return 'placeholder' } + Mock -CommandName Get-ADUser -MockWith { return 'placeholder' } + + It 'Should not throw an error' { + {Add-ADCommonGroupMember -Members $memberData.Name -Parameters $fakeParameters -MembersInMultipleDomains} | Should -Not -Throw + } + + It 'Should have called all mocked cmdlets' { + Assert-MockCalled -CommandName Get-ADComputer -Exactly -Times $memberData.Where( {$_.Name -like '*Computer*'}).Count + Assert-MockCalled -CommandName Get-ADUser -Exactly -Times $memberData.Where( {$_.Name -like '*Account*'}).Count + Assert-MockCalled -CommandName Get-ADGroup -Exactly -Times $memberData.Where( {$_.Name -like '*Group*'}).Count + Assert-MockCalled -CommandName Add-ADGroupMember -Exactly -Times $memberData.Count + } + } + + Context 'When the domain name cannot be determined' { + It 'Should throw an InvalidArgumentException' { + {Add-ADCommonGroupMember -Members $invalidMemberData -Parameters $fakeParameters -MembersInMultipleDomains} | Should -Throw -ExceptionType ([System.ArgumentException]) + } + } + } + #endregion + } #endregion } diff --git a/Tests/Unit/MSFT_xADGroup.Tests.ps1 b/Tests/Unit/MSFT_xADGroup.Tests.ps1 index 5b642bfd1..a8c1ed9ab 100644 --- a/Tests/Unit/MSFT_xADGroup.Tests.ps1 +++ b/Tests/Unit/MSFT_xADGroup.Tests.ps1 @@ -23,7 +23,6 @@ $TestEnvironment = Initialize-TestEnvironment ` # Begin Testing try { - #region Pester Tests # The InModuleScope command allows you to perform white-box unit testing on the internal @@ -45,6 +44,8 @@ try $testAbsentParams = $testPresentParams.Clone(); $testAbsentParams['Ensure'] = 'Absent'; + $testPresentParamsMultidomain = $testPresentParams.Clone() + $testPresentParamsMultidomain.MembershipAttribute = 'DistinguishedName' $fakeADGroup = @{ Name = $testPresentParams.GroupName; @@ -76,6 +77,12 @@ try SamAccountName = 'USER3'; SID = 'S-1-5-21-1131554080-2861379300-292325817-1108' } + $fakeADUser4 = [PSCustomObject] @{ + DistinguishedName = 'CN=User 4,CN=Users,DC=sub,DC=contoso,DC=com'; + ObjectGUID = 'ebafa34e-b020-40cd-8652-ee7286419869'; + SamAccountName = 'USER4'; + SID = 'S-1-5-21-1131554080-2861379300-292325817-1109' + } $testDomainController = 'TESTDC'; $testCredentials = New-Object System.Management.Automation.PSCredential 'DummyUser', (ConvertTo-SecureString 'DummyPassword' -AsPlainText -Force); @@ -398,23 +405,70 @@ try It "Adds group members when 'Ensure' is 'Present', the group exists and 'Members' are specified" { Mock -CommandName Get-ADGroup -MockWith { throw New-Object Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException } Mock -CommandName Set-ADGroup - Mock -CommandName Add-ADGroupMember + Mock -CommandName Add-ADCommonGroupMember Mock -CommandName New-ADGroup -MockWith { return [PSCustomObject] $fakeADGroup; } Set-TargetResource @testPresentParams -Members @($fakeADUser1.SamAccountName, $fakeADUser2.SamAccountName); - Assert-MockCalled -CommandName Add-ADGroupMember -Scope It + Assert-MockCalled -CommandName Add-ADCommonGroupMember -Scope It + } + + It "Tries to resolve the domain names for all groups in the same domain when the 'MembershipAttribute' property is set to distinguishedName" { + Mock -CommandName Get-ADGroup -MockWith { throw New-Object Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException } + Mock -CommandName Set-ADGroup + Mock -CommandName Add-ADCommonGroupMember + Mock -CommandName New-ADGroup -MockWith { return [PSCustomObject] $fakeADGroup; } + Mock -CommandName Get-DomainName -MockWith { return 'contoso.com' } + Mock -CommandName Get-ADDomainNameFromDistinguishedName -MockWith { return 'contoso.com' } + Mock -CommandName Write-Verbose -ParameterFilter { $Message -and $Message -match 'Group membership objects are in .* different AD Domains.'} + + Set-TargetResource @testPresentParamsMultidomain -Members @($fakeADUser1.distinguishedName, $fakeADUser2.distinguishedName); + + Assert-MockCalled -CommandName Get-ADDomainNameFromDistinguishedName + Assert-MockCalled -CommandName Add-ADCommonGroupMember -Scope It + Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { $Message -and $Message -match 'Group membership objects are in .* different AD Domains.'} -Exactly -Times 0 + } + + It "Tries to resolve the domain names for all groups in different domains when the 'MembershipAttribute' property is set to distinguishedName" { + Mock -CommandName Get-ADGroup -MockWith { throw New-Object Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException } + Mock -CommandName Set-ADGroup + Mock -CommandName Add-ADCommonGroupMember + Mock -CommandName New-ADGroup -MockWith { return [PSCustomObject] $fakeADGroup; } + Mock -CommandName Get-DomainName -MockWith {return 'contoso.com'} + Mock -CommandName Get-ADDomainNameFromDistinguishedName -MockWith { + param ( + [Parameter()] + [string] + $DistinguishedName + ) + + if ($DistinguishedName -match 'DC=sub') + { + return 'sub.contoso.com' + } + else + { + return 'contoso.com' + } + } + Mock -CommandName Write-Verbose -ParameterFilter { $Message -and $Message -match 'Group membership objects are in .* different AD Domains.'} + + Set-TargetResource @testPresentParamsMultidomain -Members @($fakeADUser1.distinguishedName, $fakeADUser4.distinguishedName); + + Assert-MockCalled -CommandName Get-ADDomainNameFromDistinguishedName + Assert-MockCalled -CommandName Add-ADCommonGroupMember -Scope It + Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { $Message -and $Message -match 'Group membership objects are in .* different AD Domains.'} } It "Adds group members when 'Ensure' is 'Present', the group exists and 'MembersToInclude' are specified" { Mock -CommandName Get-ADGroup -MockWith { throw New-Object Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException } Mock -CommandName Set-ADGroup - Mock -CommandName Add-ADGroupMember + Mock -CommandName Add-ADCommonGroupMember Mock -CommandName New-ADGroup -MockWith { return [PSCustomObject] $fakeADGroup; } Set-TargetResource @testPresentParams -MembersToInclude @($fakeADUser1.SamAccountName, $fakeADUser2.SamAccountName); - Assert-MockCalled -CommandName Add-ADGroupMember -Scope It + Assert-MockCalled -CommandName Add-ADCommonGroupMember -Scope It } It "Moves group when 'Ensure' is 'Present', the group exists but the 'Path' has changed" { @@ -436,13 +490,13 @@ try Mock -CommandName Get-ADGroup -MockWith { return [PSCustomObject] $fakeADGroup; } Mock -CommandName Set-ADGroup Mock -CommandName Get-ADGroupMember -MockWith { return @($fakeADUser1, $fakeADUser2); } - Mock -CommandName Add-ADGroupMember + Mock -CommandName Add-ADCommonGroupMember Mock -CommandName Remove-ADGroupMember Set-TargetResource @testPresentParams -Members $fakeADuser1.SamAccountName; Assert-MockCalled -CommandName Remove-ADGroupMember -Scope It -Exactly 1; - Assert-MockCalled -CommandName Add-ADGroupMember -Scope It -Exactly 1; + Assert-MockCalled -CommandName Add-ADCommonGroupMember -Scope It -Exactly 1; } It "Does not reset group membership when 'Ensure' is 'Present' and existing group is empty" { @@ -471,11 +525,11 @@ try Mock -CommandName Get-ADGroup -MockWith { return [PSCustomObject] $fakeADGroup; } Mock -CommandName Set-ADGroup Mock -CommandName Get-ADGroupMember -MockWith { return @($fakeADUser1, $fakeADUser2); } - Mock -CommandName Add-ADGroupMember + Mock -CommandName Add-ADCommonGroupMember Set-TargetResource @testPresentParams -MembersToInclude $fakeADuser3.SamAccountName; - Assert-MockCalled -CommandName Add-ADGroupMember -Scope It -Exactly 1; + Assert-MockCalled -CommandName Add-ADCommonGroupMember -Scope It -Exactly 1; } It "Removes group when 'Ensure' is 'Absent' and group exists" { @@ -500,7 +554,7 @@ try It "Calls 'Set-ADGroup' with credentials when 'Ensure' is 'Present' and the group does not exist (#106)" { Mock -CommandName Get-ADGroup -MockWith { throw New-Object Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException } - Mock -CommandName Set-ADGroup -ParameterFilter { $Credential -eq $testCredentials } + Mock -CommandName Set-ADGroup -ParameterFilter { $Credential -eq $testCredentials } Mock -CommandName New-ADGroup -MockWith { return [PSCustomObject] $fakeADGroup; } Set-TargetResource @testPresentParams -Credential $testCredentials; @@ -533,7 +587,7 @@ try Mock -CommandName Get-ADGroup -MockWith { return [PSCustomObject] $fakeADUniversalGroup } Mock -CommandName Set-ADGroup -ParameterFilter { $Identity -eq $fakeADUniversalGroup.Identity -and -not $PSBoundParameters.ContainsKey('GroupScope') } - Mock -CommandName Add-ADGroupMember + Mock -CommandName Add-ADCommonGroupMember Set-TargetResource -GroupName $testUniversalPresentParams.GroupName -Members @($fakeADUser1.SamAccountName, $fakeADUser2.SamAccountName) @@ -549,7 +603,7 @@ try Mock -CommandName Get-ADGroup -MockWith { return [PSCustomObject] $fakeADUniversalGroup } Mock -CommandName Set-ADGroup -ParameterFilter { $Identity -eq $fakeADUniversalGroup.Identity -and -not $PSBoundParameters.ContainsKey('GroupCategory') } - Mock -CommandName Add-ADGroupMember + Mock -CommandName Add-ADCommonGroupMember Set-TargetResource -GroupName $testUniversalPresentParams.GroupName -Members @($fakeADUser1.SamAccountName, $fakeADUser2.SamAccountName) @@ -565,7 +619,7 @@ try Mock -CommandName Get-ADGroup -MockWith { return [PSCustomObject] $fakeADUniversalGroup } Mock -CommandName Set-ADGroup -ParameterFilter { $Identity -eq $fakeADUniversalGroup.Identity -and -not $PSBoundParameters.ContainsKey('GroupScope') } - Mock -CommandName Add-ADGroupMember + Mock -CommandName Add-ADCommonGroupMember $universalGroupInCompliance = Test-TargetResource -GroupName $testUniversalPresentParams.GroupName -DisplayName $testUniversalPresentParams.DisplayName $universalGroupInCompliance | Should Be $true @@ -580,7 +634,7 @@ try Mock -CommandName Get-ADGroup -MockWith { return [PSCustomObject] $fakeADUniversalGroup } Mock -CommandName Set-ADGroup -ParameterFilter { $Identity -eq $fakeADUniversalGroup.Identity -and -not $PSBoundParameters.ContainsKey('GroupScope') } - Mock -CommandName Add-ADGroupMember + Mock -CommandName Add-ADCommonGroupMember $universalGroupInCompliance = Test-TargetResource -GroupName $testUniversalPresentParams.GroupName -DisplayName $testUniversalPresentParams.DisplayName $universalGroupInCompliance | Should Be $true