From 87b1308891fb28d092aa12eb886661cd15d5d018 Mon Sep 17 00:00:00 2001 From: Jeremy Ciak <51718240+jeremyciak@users.noreply.github.com> Date: Sat, 10 Oct 2020 04:16:03 -0400 Subject: [PATCH] ADGroup: Changing group membership management mechanism (#620) This is intended to change the way that the ADGroup resource manages group membership. The new implementation abandons usage of Add-ADGroupMember and Remove-ADGroupMember due to limitations with Foreign Security Principals. Instead we opt to utilize Set-ADGroup with the Add and Remove parameters, passing a hash object with the member key and a list of formatted SID values (e.g. - ""). --- CHANGELOG.md | 5 +- .../MSFT_ADGroup/MSFT_ADGroup.psm1 | 51 +- .../4-ADGroup_NewGroupOneWayTrust_Config.ps1 | 41 ++ .../ActiveDirectoryDsc.Common.psd1 | 4 +- .../ActiveDirectoryDsc.Common.psm1 | 324 +++++++--- .../ActiveDirectoryDsc.Common/README.md | 12 +- .../docs/Add-ADCommonGroupMember.md | 92 --- .../docs/Get-ADCommonParameters.md | 2 +- .../docs/Resolve-MembersSecurityIdentifier.md | 111 ++++ .../docs/Resolve-SecurityIdentifier.md | 54 ++ .../docs/Set-ADCommonGroupMember.md | 109 ++++ .../ActiveDirectoryDsc.Common.strings.psd1 | 11 +- .../Unit/ActiveDirectoryDsc.Common.Tests.ps1 | 563 ++++++++++++------ tests/Unit/MSFT_ADGroup.Tests.ps1 | 84 ++- 14 files changed, 1082 insertions(+), 381 deletions(-) create mode 100644 source/Examples/Resources/ADGroup/4-ADGroup_NewGroupOneWayTrust_Config.ps1 delete mode 100644 source/Modules/ActiveDirectoryDsc.Common/docs/Add-ADCommonGroupMember.md create mode 100644 source/Modules/ActiveDirectoryDsc.Common/docs/Resolve-MembersSecurityIdentifier.md create mode 100644 source/Modules/ActiveDirectoryDsc.Common/docs/Resolve-SecurityIdentifier.md create mode 100644 source/Modules/ActiveDirectoryDsc.Common/docs/Set-ADCommonGroupMember.md diff --git a/CHANGELOG.md b/CHANGELOG.md index dd332293b..e278f8338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,10 @@ For older change log history see the [historic changelog](HISTORIC_CHANGELOG.md) ## [Unreleased] ### Added - +- ADGroup + - Added support for managing AD group membership of Foreign Security Principals. This involved completely + refactoring group membership management to utilize the `Set-ADGroup` cmdlet and referencing SID values. + ([issue #619](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/619)). - ADFineGrainedPasswordPolicy - New resource for creating and updating Fine Grained Password Policies for AD principal subjects. ([issue #584](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/584)). diff --git a/source/DSCResources/MSFT_ADGroup/MSFT_ADGroup.psm1 b/source/DSCResources/MSFT_ADGroup/MSFT_ADGroup.psm1 index fa9ef695a..66d42a955 100644 --- a/source/DSCResources/MSFT_ADGroup/MSFT_ADGroup.psm1 +++ b/source/DSCResources/MSFT_ADGroup/MSFT_ADGroup.psm1 @@ -655,8 +655,6 @@ function Set-TargetResource Assert-MemberParameters @assertMemberParameters - $membersInMultipleDomains = $false - if ($MembershipAttribute -eq 'DistinguishedName') { $allMembers = $Members + $MembersToInclude + $MembersToExclude @@ -676,7 +674,6 @@ function Set-TargetResource if ($GroupMemberDomainCount -gt 1 -or ($groupMemberDomains -ine (Get-DomainName)).Count -gt 0) { Write-Verbose -Message ($script:localizedData.GroupMembershipMultipleDomains -f $GroupMemberDomainCount) - $membersInMultipleDomains = $true } } @@ -842,12 +839,24 @@ function Set-TargetResource { Write-Verbose -Message ($script:localizedData.RemovingGroupMembers -f $adGroupMembers.Count, $GroupName) - Remove-ADGroupMember @commonParameters -Members $adGroupMembers -Confirm:$false -ErrorAction 'Stop' + $setADCommonGroupMemberParms = @{ + Members = $adGroupMembers + MembershipAttribute = $MembershipAttribute + Parameters = $commonParameters + Action = 'Remove' + } + Set-ADCommonGroupMember @setADCommonGroupMemberParms } Write-Verbose -Message ($script:localizedData.AddingGroupMembers -f $Members.Count, $GroupName) - Add-ADCommonGroupMember -Parameters $commonParameters -Members $Members -MembersInMultipleDomains:$membersInMultipleDomains + $setADCommonGroupMemberParms = @{ + Members = $Members + MembershipAttribute = $MembershipAttribute + Parameters = $commonParameters + Action = 'Add' + } + Set-ADCommonGroupMember @setADCommonGroupMemberParms } if ($PSBoundParameters.ContainsKey('MembersToInclude') -and -not [System.String]::IsNullOrEmpty($MembersToInclude)) @@ -856,7 +865,13 @@ function Set-TargetResource Write-Verbose -Message ($script:localizedData.AddingGroupMembers -f $MembersToInclude.Count, $GroupName) - Add-ADCommonGroupMember -Parameters $commonParameters -Members $MembersToInclude -MembersInMultipleDomains:$membersInMultipleDomains + $setADCommonGroupMemberParms = @{ + Members = $MembersToInclude + MembershipAttribute = $MembershipAttribute + Parameters = $commonParameters + Action = 'Add' + } + Set-ADCommonGroupMember @setADCommonGroupMemberParms } if ($PSBoundParameters.ContainsKey('MembersToExclude') -and -not [System.String]::IsNullOrEmpty($MembersToExclude)) @@ -865,7 +880,13 @@ function Set-TargetResource Write-Verbose -Message ($script:localizedData.RemovingGroupMembers -f $MembersToExclude.Count, $GroupName) - Remove-ADGroupMember @commonParameters -Members $MembersToExclude -Confirm:$false -ErrorAction 'Stop' + $setADCommonGroupMemberParms = @{ + Members = $MembersToExclude + MembershipAttribute = $MembershipAttribute + Parameters = $commonParameters + Action = 'Remove' + } + Set-ADCommonGroupMember @setADCommonGroupMemberParms } } } @@ -960,7 +981,13 @@ function Set-TargetResource Write-Verbose -Message ($script:localizedData.AddingGroupMembers -f $Members.Count, $GroupName) - Add-ADCommonGroupMember -Parameters $commonParameters -Members $Members -MembersInMultipleDomains:$membersInMultipleDomains + $setADCommonGroupMemberParms = @{ + Members = $Members + MembershipAttribute = $MembershipAttribute + Parameters = $commonParameters + Action = 'Add' + } + Set-ADCommonGroupMember @setADCommonGroupMemberParms } elseif ($PSBoundParameters.ContainsKey('MembersToInclude') -and -not [System.String]::IsNullOrEmpty($MembersToInclude)) { @@ -968,7 +995,13 @@ function Set-TargetResource Write-Verbose -Message ($script:localizedData.AddingGroupMembers -f $MembersToInclude.Count, $GroupName) - Add-ADCommonGroupMember -Parameters $commonParameters -Members $MembersToInclude -MembersInMultipleDomains:$membersInMultipleDomains + $setADCommonGroupMemberParms = @{ + Members = $MembersToInclude + MembershipAttribute = $MembershipAttribute + Parameters = $commonParameters + Action = 'Add' + } + Set-ADCommonGroupMember @setADCommonGroupMemberParms } } } #end catch diff --git a/source/Examples/Resources/ADGroup/4-ADGroup_NewGroupOneWayTrust_Config.ps1 b/source/Examples/Resources/ADGroup/4-ADGroup_NewGroupOneWayTrust_Config.ps1 new file mode 100644 index 000000000..48037d42d --- /dev/null +++ b/source/Examples/Resources/ADGroup/4-ADGroup_NewGroupOneWayTrust_Config.ps1 @@ -0,0 +1,41 @@ +<#PSScriptInfo +.VERSION 1.0.0 +.GUID f2ecc331-e242-4204-a6b1-54fd68c852b7 +.AUTHOR DSC Community +.COMPANYNAME DSC Community +.COPYRIGHT DSC Community contributors. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/dsccommunity/ActiveDirectoryDsc/blob/master/LICENSE +.PROJECTURI https://github.com/dsccommunity/ActiveDirectoryDsc +.ICONURI https://dsccommunity.org/images/DSC_Logo_300p.png +.RELEASENOTES +Initial release +#> + +#Requires -Module ActiveDirectoryDsc + +<# + .DESCRIPTION + This configuration will create a new domain-local group in contoso with + two members; one from the contoso domain and one from the fabrikam domain. + This qualified SamAccountName format is required if any of the users are in a + one-way trusted forest/external domain. +#> +Configuration ADGroup_NewGroupOneWayTrust_Config +{ + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + ADGroup 'ExampleExternalTrustGroup' + { + GroupName = 'ExampleExternalTrustGroup' + GroupScope = 'DomainLocal' + MembershipAttribute = 'SamAccountName' + Members = @( + 'contoso\john' + 'fabrikam\toby' + ) + } + } +} diff --git a/source/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psd1 b/source/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psd1 index ffd382504..ba42df5c4 100644 --- a/source/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psd1 +++ b/source/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psd1 @@ -37,7 +37,7 @@ 'ConvertTo-DeploymentDomainMode' 'Restore-ADCommonObject' 'Get-ADDomainNameFromDistinguishedName' - 'Add-ADCommonGroupMember' + 'Set-ADCommonGroupMember' 'Get-DomainControllerObject' 'Test-IsDomainController' 'Convert-PropertyMapToObjectProperties' @@ -53,6 +53,8 @@ 'Get-ActiveDirectoryDomain' 'Get-ActiveDirectoryForest' 'Resolve-SamAccountName' + 'Resolve-SecurityIdentifier' + 'Resolve-MembersSecurityIdentifier' ) # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. diff --git a/source/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psm1 b/source/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psm1 index 4af7b4238..a0e86c619 100644 --- a/source/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psm1 +++ b/source/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psm1 @@ -965,7 +965,7 @@ function Restore-ADCommonObject ) $restoreFilter = 'msDS-LastKnownRDN -eq "{0}" -and objectClass -eq "{1}" -and isDeleted -eq $true' -f - $Identity, $ObjectClass + $Identity, $ObjectClass Write-Verbose -Message ($script:localizedData.FindInRecycleBin -f $restoreFilter) -Verbose <# @@ -1073,26 +1073,30 @@ function Get-ADDomainNameFromDistinguishedName <# .SYNOPSIS - Adds a member to an AD group. + Sets a member of an AD group by adding or removing its membership. .DESCRIPTION - The Add-ADCommonGroupMember function is used to add a member from the current or a different domain to an AD - group. + The Set-ADCommonGroupMember function is used to add a member from the current or a different domain to or remove + it from an AD group. .EXAMPLE - Add-ADCommonGroupMember -Members 'cn=user1,cn=users,dc=contoso,dc=com' -Parameters @{Identity='cn=group1,cn=users,dc=contoso,dc=com} + Set-ADCommonGroupMember -Members 'cn=user1,cn=users,dc=contoso,dc=com' -MembershipAttribute 'DistinguishedName' -Parameters @{Identity='cn=group1,cn=users,dc=contoso,dc=com'} .PARAMETER Members - Specifies the members to add to the group. These may be in the same domain as the group or in alternate - domains. + Specifies the members to add to or remove from the group. These may be in the same domain as the group or in + alternate domains. + + .PARAMETER MembershipAttribute + Specifies the Active Directory attribute for the values of the Members parameter. + Default value is 'SamAccountName'. .PARAMETER Parameters - Specifies the parameters to pass to the Add-ADGroupMember cmdlet when adding the members to the group. This - should include the group identity. + Specifies the parameters to pass to the Resolve-MembersSecurityIdentifier and Set-ADGroup cmdlets when adding + the members to the group. This should include the group Identity as well as Server and/or Credential. - .PARAMETER MembersInMultipleDomains - Setting this switch specifies that there are members from alternate domains. This triggers the identities of - the members to be looked up in the alternate domain. + .PARAMETER Action + Specifies what group membership action to take. Valid options are 'Add' and 'Remove'. + Default value is 'Add'. .INPUTS None @@ -1103,8 +1107,9 @@ function Get-ADDomainNameFromDistinguishedName .NOTES Author original code: Robert D. Biddle (https://github.com/RobBiddle) Author refactored code: Jan-Hendrik Peters (https://github.com/nyanhp) + Author refactored code: Jeremy Ciak (https://github.com/jeremyciak) #> -function Add-ADCommonGroupMember +function Set-ADCommonGroupMember { [CmdletBinding()] param @@ -1114,70 +1119,41 @@ function Add-ADCommonGroupMember $Members, [Parameter()] - [hashtable] + [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')] + [System.String] + $MembershipAttribute = 'SamAccountName', + + [Parameter()] + [System.Collections.Hashtable] $Parameters, [Parameter()] - [System.Management.Automation.SwitchParameter] - $MembersInMultipleDomains + [ValidateSet('Add', 'Remove')] + [System.String] + $Action = 'Add' ) Assert-Module -ModuleName ActiveDirectory - if ($Members) - { - if ($MembersInMultipleDomains.IsPresent) - { - foreach ($member in $Members) - { - $memberDomain = Get-ADDomainNameFromDistinguishedName -DistinguishedName $member - - if (-not $memberDomain) - { - $errorMessage = $script:localizedData.EmptyDomainError -f $member, $Parameters.Identity - New-InvalidOperationException -Message $errorMessage - } - - Write-Verbose -Message ($script:localizedData.AddingGroupMember -f $member, $memberDomain, $Parameters.Identity) - - $commonParameters = @{ - Identity = $member - Server = $memberDomain - ErrorAction = 'Stop' - } - - $activeDirectoryObject = Get-ADObject @commonParameters -Properties @('ObjectClass') - - $memberObjectClass = $activeDirectoryObject.ObjectClass + $resolveMembersSecurityIdentifierParms = @{ + MembershipAttribute = $MembershipAttribute + Parameters = $Parameters + PrepareForMembership = $true + ErrorAction = 'Stop' + } - if ($memberObjectClass -eq 'computer') - { - $memberObject = Get-ADComputer @commonParameters - } - elseif ($memberObjectClass -eq 'group') - { - $memberObject = Get-ADGroup @commonParameters - } - elseif ($memberObjectClass -eq 'user') - { - $memberObject = Get-ADUser @commonParameters - } - elseif ($memberObjectClass -eq 'msDS-ManagedServiceAccount') - { - $memberObject = Get-ADServiceAccount @commonParameters - } - elseif ($memberObjectClass -eq 'msDS-GroupManagedServiceAccount') - { - $memberObject = Get-ADServiceAccount @commonParameters - } + $Parameters[$Action] = @{ + member = $Members | Resolve-MembersSecurityIdentifier @resolveMembersSecurityIdentifierParms + } - Add-ADGroupMember @Parameters -Members $memberObject -ErrorAction 'Stop' - } - } - else - { - Add-ADGroupMember @Parameters -Members $Members -ErrorAction 'Stop' - } + try + { + Set-ADGroup @Parameters -ErrorAction 'Stop' + } + catch + { + $errorMessage = $script:localizedData.FailedToSetADGroupMembership -f $Parameters['Identity'] + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ } } @@ -2018,7 +1994,7 @@ function Find-DomainController ) } elseif ($_.Exception.InnerException -is ` - [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException]) + [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException]) { Write-Verbose -Message ($script:localizedData.FailedToFindDomainController -f $DomainName) -Verbose } @@ -2505,16 +2481,220 @@ function Resolve-SamAccountName try { - [System.Security.Principal.SecurityIdentifier]::new($ObjectSid).Translate([System.Security.Principal.NTAccount]).Value + $sidObject = [System.Security.Principal.SecurityIdentifier]::new($ObjectSid) + $sidObject.Translate([System.Security.Principal.NTAccount]).Value } catch [System.Security.Principal.IdentityNotMappedException] { - Write-Warning -Message ($script:localizedData.IdentityNotMappedExceptionError -f $ObjectSid) + Write-Warning -Message ($script:localizedData.IdentityNotMappedExceptionError -f + 'SamAccountName', 'ObjectSID', $ObjectSid) $ObjectSid } catch { - $errorMessage = $script:localizedData.ResolveSamAccountNameError -f $ObjectSid + $errorMessage = ($script:localizedData.UnableToResolveMembershipAttribute -f + 'SamAccountName', 'ObjectSID', $ObjectSid) New-InvalidResultException -Message $errorMessage -ErrorRecord $_ } } + +<# + .SYNOPSIS + Resolves the Security Identifier (SID) of an Active Directory object based on a supplied SamAccountName. + + .DESCRIPTION + The Resolve-SecurityIdentifier function is used to get a System.String object representing the Security Identifier + (SID) translated from the specified SamAccountName. + + .EXAMPLE + Resolve-SecurityIdentifier -SamAccountName $adObject.SamAccountName + + .PARAMETER SamAccountName + Specifies the Active Directory object SamAccountName to use for translation to a Security Identifier (SID). + + .INPUTS + None + + .OUTPUTS + System.String + + .NOTES + This is a wrapper to allow test mocking of the calling function. + See issue https://github.com/dsccommunity/ActiveDirectoryDsc/issues/619 for more information. +#> +function Resolve-SecurityIdentifier +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $SamAccountName + ) + + try + { + $ntAccount = [System.Security.Principal.NTAccount]::new($SamAccountName) + $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]).Value + } + catch + { + $errorMessage = ($script:localizedData.IdentityNotMappedExceptionError -f + 'SID', 'SamAccountName', $SamAccountName) + New-InvalidResultException -Message $errorMessage -ErrorRecord $_ + } +} + +<# + .SYNOPSIS + Resolves the Security Identifier (SID) of a list of Members of the same type defined by the MembershipAttribute. + + .DESCRIPTION + The Resolve-MembersSecurityIdentifier function is used to get an array of System.String objects representing + the Security Identifier (SID) translated from the specified list of Members with a type defined by the + MembershipAttribute. Custom logic is used for Foreign Security Principals to translate from a SamAccountName + or DistinguishedName, otherwise the value is sent to Get-ADObject as a filter to return the ObjectSID. + + .EXAMPLE + Get-ADGroup -Identity 'GroupName' -Properties 'Members' | Resolve-MembersSecurityIdentifier -MembershipAttribute 'DistinguishedName' + ----------- + Description + This will translate all of the DistinguishedName values for the Members of 'GroupName' into SID values. + + .PARAMETER Members + Specifies the MembershipAttribute type values representing the Members to resolve into a Security Identifier. + + .PARAMETER MembershipAttribute + Specifies the Active Directory attribute for the values of the Members parameter. + Default value is 'SamAccountName'. + + .PARAMETER Parameters + Specifies the parameters to pass to the Resolve-MembersSecurityIdentifier cmdlet for usage with the internal + Get-ADObject call. This is an optional parameter which can have Keys and Values for Server and Credential. + + .PARAMETER PrepareForMembership + Specifies whether to wrap each resulting value 'VALUE' as '' so that it can be passed directly to + Set-ADGroup under the 'member' key in the hash object. + + .INPUTS + None + + .OUTPUTS + System.String[] + + .NOTES + This is a helper function to allow for easier one-way trust AD group membership management based on SID. + See issue https://github.com/dsccommunity/ActiveDirectoryDsc/issues/619 for more information. +#> +function Resolve-MembersSecurityIdentifier +{ + [CmdletBinding()] + [OutputType([System.String[]])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [System.String[]] + $Members, + + [Parameter()] + [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')] + [System.String] + $MembershipAttribute = 'SamAccountName', + + [Parameter()] + [System.Collections.Hashtable] + $Parameters, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $PrepareForMembership + ) + + begin + { + Assert-Module -ModuleName ActiveDirectory + + $property = 'ObjectSID' + $fspADContainer = 'CN=ForeignSecurityPrincipals' + + Write-Debug -Message ($script:localizedData.ResolvingMembershipAttributeValues -f + $property, $MembershipAttribute) + + $getADObjectParms = @{} + + if ($PSBoundParameters.Keys -contains 'Parameters') + { + if (-not ([string]::IsNullOrEmpty($Parameters['Server']))) + { + $getADObjectParms['Server'] = $Parameters['Server'] + } + if ($Parameters['Credential']) + { + $getADObjectParms['Credential'] = $Parameters['Credential'] + } + } + + $getADObjectParms['Properties'] = @($property) + $getADObjectParms['ErrorAction'] = 'Stop' + } + + process + { + if ($MembershipAttribute -eq 'SID') + { + if ($PrepareForMembership.IsPresent) + { + return $Members | ForEach-Object -Process { "" } + } + else + { + return $Members + } + } + + foreach ($member in $Members) + { + if ($MembershipAttribute -eq 'SamAccountName' -and $member -match '\\') + { + Write-Debug -Message ($script:localizedData.TranslatingMembershipAttribute -f + $MembershipAttribute, $member, $property) + + $securityIdentifier = Resolve-SecurityIdentifier -SamAccountName $member + } + elseif ($MembershipAttribute -eq 'DistinguishedName' -and ($member -split ',')[1] -eq $fspADContainer) + { + Write-Debug -Message ($script:localizedData.ParsingCommonNameFromDN -f $member) + + $securityIdentifier = ($member -split ',')[0] -replace '^CN[=]' + } + else + { + Write-Debug -Message ($script:localizedData.ADObjectPropertyLookup -f + $property, $MembershipAttribute, $member) + + $getADObjectParms['Filter'] = "$($MembershipAttribute) -eq '$($member)'" + + $securityIdentifier = [string](Get-ADObject @getADObjectParms).$property + } + + if (-not ([string]::IsNullOrEmpty($securityIdentifier))) + { + if ($PrepareForMembership.IsPresent) + { + "" + } + else + { + $securityIdentifier + } + } + else + { + $errorMessage = ($script:localizedData.UnableToResolveMembershipAttribute -f + $property, $MembershipAttribute, $member) + New-InvalidOperationException -Message $errorMessage + } + } + } +} diff --git a/source/Modules/ActiveDirectoryDsc.Common/README.md b/source/Modules/ActiveDirectoryDsc.Common/README.md index ed1624945..3aaf0d2f9 100644 --- a/source/Modules/ActiveDirectoryDsc.Common/README.md +++ b/source/Modules/ActiveDirectoryDsc.Common/README.md @@ -4,9 +4,6 @@ The ActiveDirectoryDsc.Common module is a PowerShell module that contains a set of functions that are common across the ActiveDirectoryDsc Module ## ActiveDirectoryDsc.Common Cmdlets -### [Add-ADCommonGroupMember](docs/Add-ADCommonGroupMember.md) -Adds a member to an AD group. - ### [Add-TypeAssembly](docs/Add-TypeAssembly.md) Adds the assembly to the PowerShell session. @@ -73,12 +70,21 @@ Creates a new MSFT_Credential CIM instance credential object. ### [Remove-DuplicateMembers](docs/Remove-DuplicateMembers.md) Removes duplicate members from a string array. +### [Resolve-MembersSecurityIdentifier](docs/Resolve-MembersSecurityIdentifier.md) +Resolves the Security Identifier (docs/SID) of a list of Members of the same type defined by the MembershipAttribute. + ### [Resolve-SamAccountName](docs/Resolve-SamAccountName.md) Resolves the SamAccountName of an Active Directory object based on a supplied ObjectSid. +### [Resolve-SecurityIdentifier](docs/Resolve-SecurityIdentifier.md) +Resolves the Security Identifier (docs/SID) of an Active Directory object based on a supplied SamAccountName. + ### [Restore-ADCommonObject](docs/Restore-ADCommonObject.md) Restores an AD object from the AD recyle bin. +### [Set-ADCommonGroupMember](docs/Set-ADCommonGroupMember.md) +Sets a member of an AD group by adding or removing its membership. + ### [Start-ProcessWithTimeout](docs/Start-ProcessWithTimeout.md) Starts a process with a timeout. diff --git a/source/Modules/ActiveDirectoryDsc.Common/docs/Add-ADCommonGroupMember.md b/source/Modules/ActiveDirectoryDsc.Common/docs/Add-ADCommonGroupMember.md deleted file mode 100644 index 690903523..000000000 --- a/source/Modules/ActiveDirectoryDsc.Common/docs/Add-ADCommonGroupMember.md +++ /dev/null @@ -1,92 +0,0 @@ - -# Add-ADCommonGroupMember - -## SYNOPSIS -Adds a member to an AD group. - -## SYNTAX - -``` -Add-ADCommonGroupMember [[-Members] ] [[-Parameters] ] [-MembersInMultipleDomains] - [] -``` - -## DESCRIPTION -The Add-ADCommonGroupMember function is used to add a member from the current or a different domain to an AD -group. - -## EXAMPLES - -### EXAMPLE 1 -``` -Add-ADCommonGroupMember -Members 'cn=user1,cn=users,dc=contoso,dc=com' -Parameters @{Identity='cn=group1,cn=users,dc=contoso,dc=com} -``` - -## PARAMETERS - -### -Members -Specifies the members to add to the group. -These may be in the same domain as the group or in alternate -domains. - -```yaml -Type: System.String[] -Parameter Sets: (All) -Aliases: - -Required: False -Position: 1 -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - -### -MembersInMultipleDomains -Setting this switch specifies that there are members from alternate domains. -This triggers the identities of -the members to be looked up in the alternate domain. - -```yaml -Type: System.Management.Automation.SwitchParameter -Parameter Sets: (All) -Aliases: - -Required: False -Position: Named -Default value: False -Accept pipeline input: False -Accept wildcard characters: False -``` - -### -Parameters -Specifies the parameters to pass to the Add-ADGroupMember cmdlet when adding the members to the group. -This -should include the group identity. - -```yaml -Type: System.Collections.Hashtable -Parameter Sets: (All) -Aliases: - -Required: False -Position: 2 -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - -### CommonParameters -This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). - -## INPUTS - -### None -## OUTPUTS - -### None -## NOTES -Author original code: Robert D. -Biddle (https://github.com/RobBiddle) -Author refactored code: Jan-Hendrik Peters (https://github.com/nyanhp) - -## RELATED LINKS diff --git a/source/Modules/ActiveDirectoryDsc.Common/docs/Get-ADCommonParameters.md b/source/Modules/ActiveDirectoryDsc.Common/docs/Get-ADCommonParameters.md index 2470383ee..0ad7a2112 100644 --- a/source/Modules/ActiveDirectoryDsc.Common/docs/Get-ADCommonParameters.md +++ b/source/Modules/ActiveDirectoryDsc.Common/docs/Get-ADCommonParameters.md @@ -76,7 +76,7 @@ Aliases are 'UserName', ```yaml Type: System.String Parameter Sets: (All) -Aliases: UserName, GroupName, ComputerName, ServiceAccountName +Aliases: UserName, GroupName, ComputerName, ServiceAccountName, Name Required: True Position: 1 diff --git a/source/Modules/ActiveDirectoryDsc.Common/docs/Resolve-MembersSecurityIdentifier.md b/source/Modules/ActiveDirectoryDsc.Common/docs/Resolve-MembersSecurityIdentifier.md new file mode 100644 index 000000000..d2b35e150 --- /dev/null +++ b/source/Modules/ActiveDirectoryDsc.Common/docs/Resolve-MembersSecurityIdentifier.md @@ -0,0 +1,111 @@ + +# Resolve-MembersSecurityIdentifier + +## SYNOPSIS +Resolves the Security Identifier (SID) of a list of Members of the same type defined by the MembershipAttribute. + +## SYNTAX + +``` +Resolve-MembersSecurityIdentifier [-Members] [[-MembershipAttribute] ] + [[-Parameters] ] [-PrepareForMembership] [] +``` + +## DESCRIPTION +The Resolve-MembersSecurityIdentifier function is used to get an array of System.String objects representing +the Security Identifier (SID) translated from the specified list of Members with a type defined by the +MembershipAttribute. +Custom logic is used for Foreign Security Principals to translate from a SamAccountName +or DistinguishedName, otherwise the value is sent to Get-ADObject as a filter to return the ObjectSID. + +## EXAMPLES + +### EXAMPLE 1 +``` +Get-ADGroup -Identity 'GroupName' -Properties 'Members' | Resolve-MembersSecurityIdentifier -MembershipAttribute 'DistinguishedName' +``` + +----------- +Description +This will translate all of the DistinguishedName values for the Members of 'GroupName' into SID values. + +## PARAMETERS + +### -Members +Specifies the MembershipAttribute type values representing the Members to resolve into a Security Identifier. + +```yaml +Type: System.String[] +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -MembershipAttribute +Specifies the Active Directory attribute for the values of the Members parameter. +Default value is 'SamAccountName'. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: SamAccountName +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Parameters +Specifies the parameters to pass to the Resolve-MembersSecurityIdentifier cmdlet for usage with the internal +Get-ADObject call. +This is an optional parameter which can have Keys and Values for Server and Credential. + +```yaml +Type: System.Collections.Hashtable +Parameter Sets: (All) +Aliases: + +Required: False +Position: 3 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -PrepareForMembership +Specifies whether to wrap each resulting value 'VALUE' as '\' so that it can be passed directly to +Set-ADGroup under the 'member' key in the hash object. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None +## OUTPUTS + +### System.String[] +## NOTES +This is a helper function to allow for easier one-way trust AD group membership management based on SID. +See issue https://github.com/dsccommunity/ActiveDirectoryDsc/issues/619 for more information. + +## RELATED LINKS diff --git a/source/Modules/ActiveDirectoryDsc.Common/docs/Resolve-SecurityIdentifier.md b/source/Modules/ActiveDirectoryDsc.Common/docs/Resolve-SecurityIdentifier.md new file mode 100644 index 000000000..910601e01 --- /dev/null +++ b/source/Modules/ActiveDirectoryDsc.Common/docs/Resolve-SecurityIdentifier.md @@ -0,0 +1,54 @@ + +# Resolve-SecurityIdentifier + +## SYNOPSIS +Resolves the Security Identifier (SID) of an Active Directory object based on a supplied SamAccountName. + +## SYNTAX + +``` +Resolve-SecurityIdentifier [-SamAccountName] [] +``` + +## DESCRIPTION +The Resolve-SecurityIdentifier function is used to get a System.String object representing the Security Identifier +(SID) translated from the specified SamAccountName. + +## EXAMPLES + +### EXAMPLE 1 +``` +Resolve-SecurityIdentifier -SamAccountName $adObject.SamAccountName +``` + +## PARAMETERS + +### -SamAccountName +Specifies the Active Directory object SamAccountName to use for translation to a Security Identifier (SID). + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None +## OUTPUTS + +### System.String +## NOTES +This is a wrapper to allow test mocking of the calling function. +See issue https://github.com/dsccommunity/ActiveDirectoryDsc/issues/619 for more information. + +## RELATED LINKS diff --git a/source/Modules/ActiveDirectoryDsc.Common/docs/Set-ADCommonGroupMember.md b/source/Modules/ActiveDirectoryDsc.Common/docs/Set-ADCommonGroupMember.md new file mode 100644 index 000000000..8f5ee3cf6 --- /dev/null +++ b/source/Modules/ActiveDirectoryDsc.Common/docs/Set-ADCommonGroupMember.md @@ -0,0 +1,109 @@ + +# Set-ADCommonGroupMember + +## SYNOPSIS +Sets a member of an AD group by adding or removing its membership. + +## SYNTAX + +``` +Set-ADCommonGroupMember [[-Members] ] [[-MembershipAttribute] ] [[-Parameters] ] + [[-Action] ] [] +``` + +## DESCRIPTION +The Set-ADCommonGroupMember function is used to add a member from the current or a different domain to or remove +it from an AD group. + +## EXAMPLES + +### EXAMPLE 1 +``` +Set-ADCommonGroupMember -Members 'cn=user1,cn=users,dc=contoso,dc=com' -MembershipAttribute 'DistinguishedName' -Parameters @{Identity='cn=group1,cn=users,dc=contoso,dc=com'} +``` + +## PARAMETERS + +### -Action +Specifies what group membership action to take. +Valid options are 'Add' and 'Remove'. +Default value is 'Add'. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 4 +Default value: Add +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Members +Specifies the members to add to or remove from the group. +These may be in the same domain as the group or in +alternate domains. + +```yaml +Type: System.String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -MembershipAttribute +Specifies the Active Directory attribute for the values of the Members parameter. +Default value is 'SamAccountName'. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: SamAccountName +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Parameters +Specifies the parameters to pass to the Resolve-MembersSecurityIdentifier and Set-ADGroup cmdlets when adding +the members to the group. +This should include the group Identity as well as Server and/or Credential. + +```yaml +Type: System.Collections.Hashtable +Parameter Sets: (All) +Aliases: + +Required: False +Position: 3 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None +## OUTPUTS + +### None +## NOTES +Author original code: Robert D. +Biddle (https://github.com/RobBiddle) +Author refactored code: Jan-Hendrik Peters (https://github.com/nyanhp) +Author refactored code: Jeremy Ciak (https://github.com/jeremyciak) + +## RELATED LINKS diff --git a/source/Modules/ActiveDirectoryDsc.Common/en-US/ActiveDirectoryDsc.Common.strings.psd1 b/source/Modules/ActiveDirectoryDsc.Common/en-US/ActiveDirectoryDsc.Common.strings.psd1 index c127f402f..56a70ad2a 100644 --- a/source/Modules/ActiveDirectoryDsc.Common/en-US/ActiveDirectoryDsc.Common.strings.psd1 +++ b/source/Modules/ActiveDirectoryDsc.Common/en-US/ActiveDirectoryDsc.Common.strings.psd1 @@ -14,7 +14,6 @@ ConvertFrom-StringData @' IncludeAndExcludeConflictError = The member '{0}' is included in both '{1}' and '{2}' parameter values. The same member must not be included in both '{1}' and '{2}' parameter values. (ADCOMMON0014) IncludeAndExcludeAreEmptyError = The '{0}' and '{1}' parameters are either both null or empty. At least one member must be specified in one of these parameters. (ADCOMMON0015) RecycleBinRestoreFailed = Failed restoring {0} ({1}) from the recycle bin. (ADCOMMON0017) - EmptyDomainError = No domain name retrieved for group member {0} in group {1}. (ADCOMMON0018) CheckingMembers = Checking for '{0}' members. (ADCOMMON0019) MembershipCountMismatch = Membership count is not correct. Expected '{0}' members, actual '{1}' members. (ADCOMMON0020) MemberNotInDesiredState = Member '{0}' is not in the desired state. (ADCOMMON0021) @@ -24,7 +23,7 @@ ConvertFrom-StringData @' FindInRecycleBin = Finding objects in the recycle bin matching the filter {0}. (ADCOMMON0027) FoundRestoreTargetInRecycleBin = Found object {0} ({1}) in the recycle bin as {2}. Attempting to restore the object. (ADCOMMON0028) RecycleBinRestoreSuccessful = Successfully restored object {0} ({1}) from the recycle bin. (ADCOMMON0029) - AddingGroupMember = Adding member '{0}' from domain '{1}' to AD group '{2}'. (ADCOMMON0030) + FailedToSetADGroupMembership = Unable to set the group membership for AD Group '{0}'. (ADCOMMON0030) PropertyMapArrayIsWrongType = An object in the property map array is not of the type [System.Collections.Hashtable]. (ADCOMMON0031) CreatingNewADPSDrive = Creating new AD: PSDrive. (ADCOMMON0032) CreatingNewADPSDriveError = Error creating AD: PS Drive. (ADCOMMON0033) @@ -48,6 +47,10 @@ ConvertFrom-StringData @' CreatingADDomainConnection = Creating connection to Active Directory domain '{0}'. (ADCOMMON0058) CheckingADUserPassword = Checking Active Directory user '{0}' password. (ADCOMMON0059) TestPasswordUsingImpersonation = Impersonating the credentials ''{0}'' to test password for user ''{1}''. (ADCOMMON0060) - IdentityNotMappedExceptionError = Resolving a SamAccountName from ObjectSid '{0}' failed, possibly due to an orphaned ForeignSecurityPrincipal so the ObjectSid is being returned instead. (ADCOMMON0061) - ResolveSamAccountNameError = Resolving a SamAccountName from ObjectSid '{0}' failed. (ADCOMMON0062) + IdentityNotMappedExceptionError = Translating {0} from {1} '{2}' failed. The identity is not mapped. (ADCOMMON0061) + UnableToResolveMembershipAttribute = Unable to resolve {0} value from {1} '{2}'. (ADCOMMON0062) + ResolvingMembershipAttributeValues = Resolving {0} values based on supplied {1} values. (ADCOMMON0063) + TranslatingMembershipAttribute = Translating {0} value '{1}' to {2}. (ADCOMMON0064) + ParsingCommonNameFromDN = Parsing CommonName value from DistinguishedName '{0}'. (ADCOMMON0065) + ADObjectPropertyLookup = Looking up AD Object based on {0} '{1}' to retrieve {2} value. (ADCOMMON0066) '@ diff --git a/tests/Unit/ActiveDirectoryDsc.Common.Tests.ps1 b/tests/Unit/ActiveDirectoryDsc.Common.Tests.ps1 index 74eb0f930..f5c2cb7b8 100644 --- a/tests/Unit/ActiveDirectoryDsc.Common.Tests.ps1 +++ b/tests/Unit/ActiveDirectoryDsc.Common.Tests.ps1 @@ -748,186 +748,116 @@ InModuleScope 'ActiveDirectoryDsc.Common' { } } - Describe 'ActiveDirectoryDsc.Common\Add-ADCommonGroupMember' { - Mock -CommandName Assert-Module -ParameterFilter { $ModuleName -eq 'ActiveDirectory' } + Describe 'ActiveDirectoryDsc.Common\Set-ADCommonGroupMember' { + BeforeAll { + $mockADGroupMembersAsADObjects = @( + [PSCustomObject] @{ + DistinguishedName = 'CN=User 1,CN=Users,DC=contoso,DC=com' + ObjectGUID = 'a97cc867-0c9e-4928-8387-0dba0c883b8e' + SamAccountName = 'USER1' + ObjectSID = 'S-1-5-21-1131554080-2861379300-292325817-1106' + ObjectClass = 'user' + } + [PSCustomObject] @{ + DistinguishedName = 'CN=Group 1,CN=Users,DC=contoso,DC=com' + ObjectGUID = 'e2328767-2673-40b2-b3b7-ce9e6511df06' + SamAccountName = 'GROUP1' + ObjectSID = 'S-1-5-21-1131554080-2861379300-292325817-1206' + ObjectClass = 'group' + } + [PSCustomObject] @{ + DistinguishedName = 'CN=Computer 1,CN=Users,DC=contoso,DC=com' + ObjectGUID = '42f9d607-0934-4afc-bb91-bdf93e07cbfc' + SamAccountName = 'COMPUTER1' + ObjectSID = 'S-1-5-21-1131554080-2861379300-292325817-6606' + ObjectClass = 'computer' + } + # This entry is used to represent a group member from a one-way trusted domain + [PSCustomObject] @{ + DistinguishedName = 'CN=S-1-5-21-8562719340-2451078396-046517832-2106,CN=ForeignSecurityPrincipals,DC=contoso,DC=com' + ObjectGUID = '6df78e9e-c795-4e67-a626-e17f1b4a0d8b' + SamAccountName = 'ADATUM\USER1' + ObjectSID = 'S-1-5-21-8562719340-2451078396-046517832-2106' + ObjectClass = 'foreignSecurityPrincipal' + } + ) - $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=Msa1,DC=contoso,DC=com' - Domain = 'contoso.com' - } - [PSCustomObject] @{ - Name = 'CN=Gmsa1,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=Msa1,DC=a,DC=contoso,DC=com' - Domain = 'a.contoso.com' - } - [PSCustomObject] @{ - Name = 'CN=Gmsa1,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' - } - [PSCustomObject] @{ - Name = 'CN=Msa1,DC=b,DC=contoso,DC=com' - Domain = 'b.contoso.com' - } - [PSCustomObject] @{ - Name = 'CN=Gmsa1,DC=b,DC=contoso,DC=com' - Domain = 'b.contoso.com' + $setADCommonGroupMemberParms = @{ + Members = $mockADGroupMembersAsADObjects.DistinguishedName + MembershipAttribute = 'DistinguishedName' + Parameters = @{ + Identity = 'CN=TestGroup,OU=Fake,DC=contoso,DC=com' + } } - ) - $invalidMemberData = @( - 'contoso.com\group1' - 'user1@contoso.com' - 'computer1.contoso.com' - ) + $membershipSID = @{ + member = $mockADGroupMembersAsADObjects.ObjectSID | ForEach-Object -Process { "" } + } - $fakeParameters = @{ - Identity = 'SomeGroup' + Mock -CommandName Assert-Module + Mock -CommandName Resolve-MembersSecurityIdentifier -MockWith { $membershipSID['member'] } + Mock -CommandName Set-ADGroup } - Context 'When all members are in the same domain' { + Context "When the 'Action' parameter is specified as 'Add'" { BeforeAll { - Mock -CommandName Add-ADGroupMember - $groupCount = 0 + $setADCommonGroupMemberAddParms = $setADCommonGroupMemberParms.Clone() + $setADCommonGroupMemberAddParms['Action'] = 'Add' } - foreach ($domainGroup in ($memberData | Group-Object -Property Domain)) - { - $groupCount ++ - It "Should not throw an error for $($domainGroup.Name)" { - { Add-ADCommonGroupMember -Members $domainGroup.Group.Name -Parameters $fakeParameters } | - Should -Not -Throw - } + It 'Should not throw' { + { Set-ADCommonGroupMember @setADCommonGroupMemberAddParms } | Should -Not -Throw } - It "Should have called Add-ADGroupMember $groupCount times" { - Assert-MockCalled -CommandName Add-ADGroupMember -Exactly -Times $groupCount + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Assert-Module ` + -ParameterFilter { $ModuleName -eq 'ActiveDirectory' } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Resolve-MembersSecurityIdentifier ` + -Exactly -Times $setADCommonGroupMemberAddParms.Members.Count + Assert-MockCalled -CommandName Set-ADGroup ` + -ParameterFilter { ` + $Add -ne $null -and ` + $Identity -eq $setADCommonGroupMemberAddParms.Parameters.Identity } ` + -Exactly -Times 1 } } - Context 'When members are in different domains' { + Context "When 'Action' parameter is specified as 'Remove'" { BeforeAll { - Mock -CommandName Add-ADGroupMember - Mock -CommandName Get-ADObject -MockWith { - param - ( - [Parameter()] - [System.String] - $Identity, - - [Parameter()] - [System.String] - $Server, - - [Parameter()] - [System.String[]] - $Properties - ) - - $objectClass = switch ($Identity) - { - { $Identity -match 'Group' } - { - 'group' - } - { $Identity -match 'Account' } - { - 'user' - } - { $Identity -match 'Computer' } - { - 'computer' - } - { $Identity -match 'msa' } - { - 'msDS-ManagedServiceAccount' - } - { $Identity -match 'gmsa' } - { - 'msDS-GroupManagedServiceAccount' - } - } - - return ( - @{ - objectClass = $objectClass - } - ) - } - # Mocks should return something that is used with Add-ADGroupMember - Mock -CommandName Get-ADComputer -MockWith { 'placeholder' } - Mock -CommandName Get-ADGroup -MockWith { 'placeholder' } - Mock -CommandName Get-ADUser -MockWith { 'placeholder' } - Mock -CommandName Get-ADServiceAccount -MockWith { 'placeholder' } + $setADCommonGroupMemberRemoveParms = $setADCommonGroupMemberParms.Clone() + $setADCommonGroupMemberRemoveParms['Action'] = 'Remove' } - It 'Should not throw an error' { - { Add-ADCommonGroupMember -Members $memberData.Name -Parameters $fakeParameters ` - -MembersInMultipleDomains } | Should -Not -Throw + It 'Should not throw' { + { Set-ADCommonGroupMember @setADCommonGroupMemberRemoveParms } | Should -Not -Throw } - It 'Should have called the expected mocks' { - 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 Get-ADServiceAccount ` - -Exactly -Times $memberData.Where( { $_.Name -like '*msa*' }).Count - Assert-MockCalled -CommandName Add-ADGroupMember ` - -Exactly -Times $memberData.Count + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Assert-Module ` + -ParameterFilter { $ModuleName -eq 'ActiveDirectory' } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Resolve-MembersSecurityIdentifier ` + -Exactly -Times $setADCommonGroupMemberRemoveParms.Members.Count + Assert-MockCalled -CommandName Set-ADGroup ` + -ParameterFilter { ` + $Remove -ne $null -and ` + $Identity -eq $setADCommonGroupMemberRemoveParms.Parameters.Identity } ` + -Exactly -Times 1 } } - Context 'When the domain name cannot be determined' { + Context "When 'Set-ADGroup' throws an exception" { BeforeAll { - $emptyDomainError = ($script:localizedData.EmptyDomainError -f - $invalidMemberData[0], $fakeParameters.Identity) + Mock -CommandName Set-ADGroup -MockWith { throw 'Error' } + + $errorMessage = $script:localizedData.FailedToSetADGroupMembership -f + $setADCommonGroupMemberParms.Parameters.Identity } - It 'Should throw the correct error' { - { Add-ADCommonGroupMember -Members $invalidMemberData -Parameters $fakeParameters ` - -MembersInMultipleDomains } | Should -Throw $emptyDomainError + It "Should throw the correct exception" { + { Set-ADCommonGroupMember @setADCommonGroupMemberParms } | + Should -Throw $errorMessage } } } @@ -2211,27 +2141,320 @@ InModuleScope 'ActiveDirectoryDsc.Common' { } } - Describe 'ActiveDirectoryDsc.Common\Resolve-SamAccountName' { - Context 'Properly formatted ObjectSid' { - $objectSid = 'S-1-5-21-8562719340-2451078396-046517832-2106' + Describe 'ActiveDirectoryDsc.Common\Resolve-MembersSecurityIdentifier' { + $mockADGroupMembersAsADObjects = @( + [PSCustomObject] @{ + DistinguishedName = 'CN=User 1,CN=Users,DC=contoso,DC=com' + ObjectGUID = 'a97cc867-0c9e-4928-8387-0dba0c883b8e' + SamAccountName = 'USER1' + ObjectSID = 'S-1-5-21-1131554080-2861379300-292325817-1106' + ObjectClass = 'user' + } + [PSCustomObject] @{ + DistinguishedName = 'CN=Group 1,CN=Users,DC=contoso,DC=com' + ObjectGUID = 'e2328767-2673-40b2-b3b7-ce9e6511df06' + SamAccountName = 'GROUP1' + ObjectSID = 'S-1-5-21-1131554080-2861379300-292325817-1206' + ObjectClass = 'group' + } + [PSCustomObject] @{ + DistinguishedName = 'CN=Computer 1,CN=Users,DC=contoso,DC=com' + ObjectGUID = '42f9d607-0934-4afc-bb91-bdf93e07cbfc' + SamAccountName = 'COMPUTER1' + ObjectSID = 'S-1-5-21-1131554080-2861379300-292325817-6606' + ObjectClass = 'computer' + } + # This entry is used to represent a group member from a one-way trusted domain + [PSCustomObject] @{ + DistinguishedName = 'CN=S-1-5-21-8562719340-2451078396-046517832-2106,CN=ForeignSecurityPrincipals,DC=contoso,DC=com' + ObjectGUID = '6df78e9e-c795-4e67-a626-e17f1b4a0d8b' + SamAccountName = 'ADATUM\USER1' + ObjectSID = 'S-1-5-21-8562719340-2451078396-046517832-2106' + ObjectClass = 'foreignSecurityPrincipal' + } + ) + + BeforeAll { + $script:memberIndex = 0 + + Mock -CommandName Assert-Module + + Mock -CommandName Resolve-SecurityIdentifier -MockWith { + $memberADObjectSID = $mockADGroupMembersAsADObjects[($script:memberIndex)].ObjectSID + $script:memberIndex++ + return $memberADObjectSID + } + + Mock -CommandName Get-ADObject -MockWith { + $memberADObject = $mockADGroupMembersAsADObjects[$script:memberIndex] + $script:memberIndex++ + return $memberADObject + } + } + + Context "When 'Server' is passed as part of the 'Parameters' parameter" { + BeforeAll { + $testServer = 'TESTDC' + $membershipAttribute = 'ObjectGUID' + + $script:memberIndex = 0 + + $resolveMembersSecurityIdentifierParms = @{ + Members = $mockADGroupMembersAsADObjects.$membershipAttribute + MembershipAttribute = $membershipAttribute + Parameters = @{ + Server = $testServer + } + } + } + + It 'Should not throw' { + { Resolve-MembersSecurityIdentifier @resolveMembersSecurityIdentifierParms } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Assert-Module ` + -ParameterFilter { $ModuleName -eq 'ActiveDirectory' } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Resolve-SecurityIdentifier ` + -Exactly -Times 0 + Assert-MockCalled -CommandName Get-ADObject ` + -ParameterFilter { $Server -eq $testServer } ` + -Exactly -Times $mockADGroupMembersAsADObjects.Count + } + } + + Context "When 'Credential' is passed as part of the 'Parameters' parameter" { + BeforeAll { + $testCredentials = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList @( + 'DummyUser', + (ConvertTo-SecureString -String 'DummyPassword' -AsPlainText -Force) + ) + $membershipAttribute = 'ObjectGUID' + + $script:memberIndex = 0 + + $resolveMembersSecurityIdentifierParms = @{ + Members = $mockADGroupMembersAsADObjects.$membershipAttribute + MembershipAttribute = $membershipAttribute + Parameters = @{ + Credential = $testCredentials + } + } + } + + It 'Should not throw' { + { Resolve-MembersSecurityIdentifier @resolveMembersSecurityIdentifierParms } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Assert-Module ` + -ParameterFilter { $ModuleName -eq 'ActiveDirectory' } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Resolve-SecurityIdentifier ` + -Exactly -Times 0 + Assert-MockCalled -CommandName Get-ADObject ` + -ParameterFilter { $Credential -eq $testCredentials } ` + -Exactly -Times $mockADGroupMembersAsADObjects.Count + } + } + + Context "When 'Get-ADObject' returns no value" { + BeforeAll { + $membershipAttribute = 'ObjectGUID' - It 'Should not throw and assume an orphaned ForeignSecurityPrincipal' { - { Resolve-SamAccountName -ObjectSid $objectSid -ErrorAction Stop -WarningAction SilentlyContinue } | - Should -Not -Throw + $resolveMembersSecurityIdentifierParms = @{ + Members = $mockADGroupMembersAsADObjects[0].$membershipAttribute + MembershipAttribute = $membershipAttribute + } + + $errorMessage = ($script:localizedData.UnableToResolveMembershipAttribute -f + 'ObjectSID', $membershipAttribute, $mockADGroupMembersAsADObjects[0].$membershipAttribute) + + Mock -CommandName Resolve-SecurityIdentifier + Mock -CommandName Get-ADObject } - It 'Should return the ObjectSid and assume an orphaned ForeignSecurityPrincipal' { - Resolve-SamAccountName -ObjectSid $objectSid -WarningAction SilentlyContinue | - Should -Be $objectSid + + It 'Should throw the correct exception' { + { Resolve-MembersSecurityIdentifier @resolveMembersSecurityIdentifierParms } | + Should -Throw $errorMessage } } - Context 'Improperly formatted ObjectSid' { - $objectSid = (New-Guid).Guid - $errorMessage = $script:localizedData.ResolveSamAccountNameError -f $objectSid + Context "When MembershipAttribute 'SamAccountName' is specified" { + BeforeAll { + $membershipAttribute = 'SamAccountName' + + $resolveMembersSecurityIdentifierParms = @{ + Members = $mockADGroupMembersAsADObjects.$membershipAttribute + MembershipAttribute = $membershipAttribute + } + + $resolveSecurityIdentifierCount = @($mockADGroupMembersAsADObjects | + Where-Object -Property $membershipAttribute -Match '\\').Count + + $getADObjectCount = @($mockADGroupMembersAsADObjects | + Where-Object -Property $membershipAttribute -NotMatch '\\').Count + + $script:memberIndex = 0 + } + + It 'Should return the correct result' { + $result = Resolve-MembersSecurityIdentifier @resolveMembersSecurityIdentifierParms + + for ($i = 0; $i -lt $result.Count; $i++) + { + $result[$i] | Should -Be $mockADGroupMembersAsADObjects[$i].ObjectSID + } + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Assert-Module ` + -ParameterFilter { $ModuleName -eq 'ActiveDirectory' } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Resolve-SecurityIdentifier ` + -Exactly -Times $resolveSecurityIdentifierCount + Assert-MockCalled -CommandName Get-ADObject ` + -Exactly -Times $getADObjectCount + } + } + + Context "When MembershipAttribute 'DistinguishedName' is specified" { + BeforeAll { + $membershipAttribute = 'DistinguishedName' + + $resolveMembersSecurityIdentifierParms = @{ + Members = $mockADGroupMembersAsADObjects.$membershipAttribute + MembershipAttribute = $membershipAttribute + } + + $getADObjectCount = @($mockADGroupMembersAsADObjects | + Where-Object -Property $membershipAttribute -NotMatch 'CN=ForeignSecurityPrincipals').Count + + $script:memberIndex = 0 + } + + It 'Should return the correct result' { + $result = Resolve-MembersSecurityIdentifier @resolveMembersSecurityIdentifierParms + + for ($i = 0; $i -lt $result.Count; $i++) + { + $result[$i] | Should -Be $mockADGroupMembersAsADObjects[$i].ObjectSID + } + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Assert-Module ` + -ParameterFilter { $ModuleName -eq 'ActiveDirectory' } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Resolve-SecurityIdentifier ` + -Exactly -Times 0 + Assert-MockCalled -CommandName Get-ADObject ` + -Exactly -Times $getADObjectCount + } + } - It 'Should throw and not assume an orphaned ForeignSecurityPrincipal' { - { Resolve-SamAccountName -ObjectSid $objectSid -ErrorAction Stop } | - Should Throw $errorMessage + Context "When MembershipAttribute 'ObjectGUID' is specified" { + BeforeAll { + $membershipAttribute = 'ObjectGUID' + + $resolveMembersSecurityIdentifierParms = @{ + Members = $mockADGroupMembersAsADObjects.$membershipAttribute + MembershipAttribute = $membershipAttribute + } + + $script:memberIndex = 0 + } + + It 'Should Return the correct result' { + $result = Resolve-MembersSecurityIdentifier @resolveMembersSecurityIdentifierParms + + for ($i = 0; $i -lt $result.Count; $i++) + { + $result[$i] | Should -Be $mockADGroupMembersAsADObjects[$i].ObjectSID + } + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Assert-Module ` + -ParameterFilter { $ModuleName -eq 'ActiveDirectory' } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Resolve-SecurityIdentifier ` + -Exactly -Times 0 + Assert-MockCalled -CommandName Get-ADObject ` + -Exactly -Times $mockADGroupMembersAsADObjects.Count + } + } + + Context "When MembershipAttribute 'SID' is specified" { + BeforeAll { + $resolveMembersSecurityIdentifierParms = @{ + Members = $mockADGroupMembersAsADObjects.ObjectSID + MembershipAttribute = 'SID' + } + + $script:memberIndex = 0 + } + + It 'Should return the correct result' { + $result = Resolve-MembersSecurityIdentifier @resolveMembersSecurityIdentifierParms + + for ($i = 0; $i -lt $result.Count; $i++) + { + $result[$i] | Should -Be $mockADGroupMembersAsADObjects[$i].ObjectSID + } + } + + It 'Should call the expected mocks' { + Assert-MockCalled -CommandName Assert-Module ` + -ParameterFilter { $ModuleName -eq 'ActiveDirectory' } ` + -Exactly -Times 1 + Assert-MockCalled -CommandName Resolve-SecurityIdentifier ` + -Exactly -Times 0 + Assert-MockCalled -CommandName Get-ADObject ` + -Exactly -Times 0 + } + } + + Context "When 'PrepareForMembership' is specified" { + Context "When the MembershipAttribute specified is not 'SID'" { + BeforeAll { + $membershipAttribute = 'ObjectGUID' + + $resolveMembersSecurityIdentifierParms = @{ + Members = $mockADGroupMembersAsADObjects.$membershipAttribute + MembershipAttribute = $membershipAttribute + PrepareForMembership = $true + } + } + + It 'Should return the correct result' { + $result = Resolve-MembersSecurityIdentifier @resolveMembersSecurityIdentifierParms + + for ($i = 0; $i -lt $result.Count; $i++) + { + $result[$i] | Should -Be "" + } + } + } + + Context "When MembershipAttribute specified is 'SID'" { + BeforeAll { + $resolveMembersSecurityIdentifierParms = @{ + Members = $mockADGroupMembersAsADObjects.ObjectSID + MembershipAttribute = 'SID' + PrepareForMembership = $true + } + } + + It 'Should return the correct result' { + $result = Resolve-MembersSecurityIdentifier @resolveMembersSecurityIdentifierParms + + for ($i = 0; $i -lt $result.Count; $i++) + { + $result[$i] | Should -Be "" + } + } } } } diff --git a/tests/Unit/MSFT_ADGroup.Tests.ps1 b/tests/Unit/MSFT_ADGroup.Tests.ps1 index 931a32c20..5f77cefd9 100644 --- a/tests/Unit/MSFT_ADGroup.Tests.ps1 +++ b/tests/Unit/MSFT_ADGroup.Tests.ps1 @@ -744,14 +744,16 @@ try } Mock -CommandName Set-ADGroup - Mock -CommandName Add-ADCommonGroupMember + Mock -CommandName Set-ADCommonGroupMember Mock -CommandName New-ADGroup -MockWith { return [PSCustomObject] $fakeADGroup } Set-TargetResource @testPresentParams -Members @($fakeADUser1.SamAccountName, $fakeADUser2.SamAccountName) - Assert-MockCalled -CommandName Add-ADCommonGroupMember -Scope It + Assert-MockCalled -CommandName Set-ADCommonGroupMember ` + -ParameterFilter { $Action -eq 'Add' } ` + -Scope It -Exactly -Times 1 } It "Tries to resolve the domain names for all groups in the same domain when the 'MembershipAttribute' property is set to distinguishedName" { @@ -760,7 +762,7 @@ try } Mock -CommandName Set-ADGroup - Mock -CommandName Add-ADCommonGroupMember + Mock -CommandName Set-ADCommonGroupMember Mock -CommandName New-ADGroup -MockWith { return [PSCustomObject] $fakeADGroup } @@ -780,7 +782,9 @@ try Set-TargetResource @testPresentParamsMultiDomain -Members @($fakeADUser1.distinguishedName, $fakeADUser2.distinguishedName) Assert-MockCalled -CommandName Get-ADDomainNameFromDistinguishedName - Assert-MockCalled -CommandName Add-ADCommonGroupMember -Scope It + Assert-MockCalled -CommandName Set-ADCommonGroupMember ` + -ParameterFilter { $Action -eq 'Add' } ` + -Scope It -Exactly -Times 1 Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { $Message -and $Message -match 'Group membership objects are in .* different AD Domains.' } -Exactly -Times 0 @@ -792,7 +796,7 @@ try } Mock -CommandName Set-ADGroup - Mock -CommandName Add-ADCommonGroupMember + Mock -CommandName Set-ADCommonGroupMember Mock -CommandName New-ADGroup -MockWith { return [PSCustomObject] $fakeADGroup } @@ -826,7 +830,9 @@ try Set-TargetResource @testPresentParamsMultiDomain -Members @($fakeADUser1.distinguishedName, $fakeADUser4.distinguishedName) Assert-MockCalled -CommandName Get-ADDomainNameFromDistinguishedName - Assert-MockCalled -CommandName Add-ADCommonGroupMember -Scope It + Assert-MockCalled -CommandName Set-ADCommonGroupMember ` + -ParameterFilter { $Action -eq 'Add' } ` + -Scope It -Exactly -Times 1 Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { $Message -and $Message -match 'Group membership objects are in .* different AD Domains.' } @@ -838,14 +844,16 @@ try } Mock -CommandName Set-ADGroup - Mock -CommandName Add-ADCommonGroupMember + Mock -CommandName Set-ADCommonGroupMember Mock -CommandName New-ADGroup -MockWith { return [PSCustomObject] $fakeADGroup } Set-TargetResource @testPresentParams -MembersToInclude @($fakeADUser1.SamAccountName, $fakeADUser2.SamAccountName) - Assert-MockCalled -CommandName Add-ADCommonGroupMember -Scope It + Assert-MockCalled -CommandName Set-ADCommonGroupMember ` + -ParameterFilter { $Action -eq 'Add' } ` + -Scope It -Exactly -Times 1 } It "Moves group when 'Ensure' is 'Present', the group exists but the 'Path' has changed" { @@ -877,13 +885,19 @@ try ) } - Mock -CommandName Add-ADCommonGroupMember - Mock -CommandName Remove-ADGroupMember + Mock -CommandName Set-ADCommonGroupMember ` + -ParameterFilter { $Action -eq 'Add' } + Mock -CommandName Set-ADCommonGroupMember ` + -ParameterFilter { $Action -eq 'Remove' } Set-TargetResource @testPresentParams -Members $fakeADUser1.SamAccountName - Assert-MockCalled -CommandName Remove-ADGroupMember -Scope It -Exactly 1 - Assert-MockCalled -CommandName Add-ADCommonGroupMember -Scope It -Exactly 1 + Assert-MockCalled -CommandName Set-ADCommonGroupMember ` + -ParameterFilter { $Action -eq 'Remove' } ` + -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Set-ADCommonGroupMember ` + -ParameterFilter { $Action -eq 'Add' } ` + -Scope It -Exactly -Times 1 } It "Does not reset group membership when 'Ensure' is 'Present' and existing group is empty" { @@ -893,11 +907,13 @@ try Mock -CommandName Set-ADGroup Mock -CommandName Get-ADGroupMember - Mock -CommandName Remove-ADGroupMember + Mock -CommandName Set-ADCommonGroupMember Set-TargetResource @testPresentParams -MembersToExclude $fakeADUser1.SamAccountName - Assert-MockCalled -CommandName Remove-ADGroupMember -Scope It -Exactly 0 + Assert-MockCalled -CommandName Set-ADCommonGroupMember ` + -ParameterFilter { $Action -eq 'Remove' } ` + -Scope It -Exactly -Times 0 } It "Removes members when 'Ensure' is 'Present' and 'MembersToExclude' is incorrect" { @@ -913,11 +929,13 @@ try ) } - Mock -CommandName Remove-ADGroupMember + Mock -CommandName Set-ADCommonGroupMember Set-TargetResource @testPresentParams -MembersToExclude $fakeADUser1.SamAccountName - Assert-MockCalled -CommandName Remove-ADGroupMember -Scope It -Exactly 1 + Assert-MockCalled -CommandName Set-ADCommonGroupMember ` + -ParameterFilter { $Action -eq 'Remove' } ` + -Scope It -Exactly -Times 1 } It "Adds members when 'Ensure' is 'Present' and 'MembersToInclude' is incorrect" { @@ -933,11 +951,13 @@ try ) } - Mock -CommandName Add-ADCommonGroupMember + Mock -CommandName Set-ADCommonGroupMember Set-TargetResource @testPresentParams -MembersToInclude $fakeADUser3.SamAccountName - Assert-MockCalled -CommandName Add-ADCommonGroupMember -Scope It -Exactly 1 + Assert-MockCalled -CommandName Set-ADCommonGroupMember ` + -ParameterFilter { $Action -eq 'Add' } ` + -Scope It -Exactly -Times 1 } It "Removes group when 'Ensure' is 'Absent' and group exists" { @@ -1028,11 +1048,17 @@ try $Identity -eq $fakeADUniversalGroup.Identity -and -not $PSBoundParameters.ContainsKey('GroupScope') } - Mock -CommandName Add-ADCommonGroupMember + Mock -CommandName Set-ADCommonGroupMember - Set-TargetResource -GroupName $testUniversalPresentParams.GroupName -Members @($fakeADUser1.SamAccountName, $fakeADUser2.SamAccountName) + Set-TargetResource -GroupName $testUniversalPresentParams.GroupName -Members @( + $fakeADUser1.SamAccountName, + $fakeADUser2.SamAccountName + ) - Assert-MockCalled -CommandName Set-ADGroup -Times 1 -Scope It + Assert-MockCalled -CommandName Set-ADGroup -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Set-ADCommonGroupMember ` + -ParameterFilter { $Action -eq 'Add' } ` + -Scope It -Exactly -Times 1 } # tests for issue 183 @@ -1050,11 +1076,17 @@ try $Identity -eq $fakeADUniversalGroup.Identity -and -not $PSBoundParameters.ContainsKey('GroupCategory') } - Mock -CommandName Add-ADCommonGroupMember + Mock -CommandName Set-ADCommonGroupMember - Set-TargetResource -GroupName $testUniversalPresentParams.GroupName -Members @($fakeADUser1.SamAccountName, $fakeADUser2.SamAccountName) + Set-TargetResource -GroupName $testUniversalPresentParams.GroupName -Members @( + $fakeADUser1.SamAccountName, + $fakeADUser2.SamAccountName + ) - Assert-MockCalled -CommandName Set-ADGroup -Times 1 -Scope It + Assert-MockCalled -CommandName Set-ADGroup -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Set-ADCommonGroupMember ` + -ParameterFilter { $Action -eq 'Add' } ` + -Scope It -Exactly -Times 1 } # tests for issue 183 @@ -1072,8 +1104,6 @@ try $Identity -eq $fakeADUniversalGroup.Identity -and -not $PSBoundParameters.ContainsKey('GroupScope') } - Mock -CommandName Add-ADCommonGroupMember - $universalGroupInCompliance = Test-TargetResource -GroupName $testUniversalPresentParams.GroupName -DisplayName $testUniversalPresentParams.DisplayName $universalGroupInCompliance | Should -BeTrue } @@ -1093,8 +1123,6 @@ try $Identity -eq $fakeADUniversalGroup.Identity -and -not $PSBoundParameters.ContainsKey('GroupScope') } - Mock -CommandName Add-ADCommonGroupMember - $universalGroupInCompliance = Test-TargetResource -GroupName $testUniversalPresentParams.GroupName -DisplayName $testUniversalPresentParams.DisplayName $universalGroupInCompliance | Should -BeTrue }