4 Managing Disposable objects
5 The types PrincipalContext, Principal, and DirectoryEntry are used througout the code
6 and all are disposable. However, in many cases, disposing the object immediately
7 causes subsequent operations to fail or duplicate dispose calls to occur.
8 To simplify management of these disposables, each public entry point defines a
9 $disposables ArrayList variable and passes it to secondary functions that may
10 need to create disposable objects. The public entry point is then required
11 to dispose the contents of the list in a finally block.
13 Managing PrincipalContext instances.
14 To use the AccountManagement APIs to connect to the local machine or a domain,
15 a PrincipalContext is needed.
17 For the local groups and users, a PrincipalContext reflecting the current user
20 For the default domain; the domain where the machine is joined, explicit credentials
21 are needed since the default user context is SYSTEM which has no rights to the
24 Additional PrincipalContext intsances may be needed when the machine is in a domain that is
25 part of a multi-domain forest. For example, Microsoft uses a multi-domain forest that
26 includes domains such as ntdev, redmond, wingroup and a group may have members that
27 span multiple domains. Unless the enterprise implements the Global Catalog;
28 something that Microsoft does not do; a unique PrincipalContext is needed to resolve
29 accounts in each of the domains.
31 To manage the use of PrincipalContext across domains, public entry points define
32 a $principalContexts hashtable and pass it to support functions that need to
33 resolve a group or group member. Consumers of a PrincipalContext call
34 GetPrincipalContext with a scope (domain name or machine name). GetPrincipalContext
35 returns an existing hashtable entry or creates a new entry. Note that a PrincipalContext
36 to a target domain requires connecting to the domain, the hash table avoids subsequent
37 connection calls. Also note that GetPrincipalContext takes a Credential parameter for the
38 case where a new PrincipalContext is needed. The implicit assumption is the credentials
39 provided for the primary domain also has rights to resolve accounts in any of the other domains.
41 Resolving Group Members
42 The original implementation assumed that group members could be resolved using the machine
43 PrincipalContext or the logged on user. In practice this is not reliable since
44 the resource is typically run under the system account and this account is not guaranteed
45 to have rights to resolve domain accounts. Additionally, the API's for enumerating group members
46 does not provide a facility for passing additional credentials resulting in domain members failing
48 To address this, group members are enumerated by first converting the GroupPrincipal to a
49 DirectoryEntry and enumerating its child members. The returned DirectoryEntry instances
50 are then resolved to Principal objects using a PrincipalContext appropriate for the
51 target domain. See ResolveGroupMembersToPrincipals for details.
53 Handling Stale Group members
54 A group may have stale members if the machine was moved from one domain to a another,
55 foreign domain or when accounts are deleted (domain or local).
56 At this point, members that were defined in the original domain or were deleted are
57 now stale and cannot be resolved using Principal::FindByIdentity. The original
58 implementation failed at this point preventing any operations against the group. The
59 current implementation calls Write-Warning with the associated SID of the member that cannot
60 be resolved then continues the operation.
64 # A global variable that contains localized messages.
68 ConvertFrom-StringData @'
69 GroupWithName=Group: {0}
70 RemoveOperation=Remove
73 GroupCreated=Group {0} created successfully.
74 GroupUpdated=Group {0} properties updated successfully.
75 GroupRemoved=Group {0} removed successfully.
76 NoConfigurationRequired=Group {0} exists on this node with the desired properties. No action required.
77 NoConfigurationRequiredGroupDoesNotExist=Group {0} does not exist on this node. No action required.
78 CouldNotFindPrincipal=Could not find a principal with the provided name [{0}]
79 MembersAndIncludeExcludeConflict=The {0} and {1} and/or {2} parameters conflict. The {0} parameter should not be used in any combination with the {1} and {2} parameters.
80 MembersIsNull=The Members parameter value is null. The {0} parameter must be provided if neither {1} nor {2} is provided.
81 MembersIsEmpty=The Members parameter is empty. At least one group member must be provided.
82 MemberNotValid=The group member does not exist or cannot be resolved: {0}.
83 IncludeAndExcludeConflict=The principal {0} is included in both {1} and {2} parameter values. The same principal must not be included in both {1} and {2} parameter values.
84 IncludeAndExcludeAreEmpty=The MembersToInclude and MembersToExclude are either both null or empty. At least one member must be specified in one of these parameters"
85 InvalidGroupName=The name {0} cannot be used. Names may not consist entirely of periods and/or spaces, or contain these characters: {1}
86 GroupExists=A group with the name {0} exists.
87 GroupDoesNotExist=A group with the name {0} does not exist.
88 PropertyMismatch=The value of the {0} property is expected to be {1} but it is {2}.
89 MembersNumberMismatch=Property {0}. The number of provided unique group members {1} is different from the number of actual group members {2}.
90 MembersMemberMismatch=At least one member {0} of the provided {1} parameter does not have a match in the existing group {2}.
91 MemberToExcludeMatch=At least one member {0} of the provided {1} parameter has a match in the existing group {2}.
92 ResolvingLocalAccount=Resolving {0} as a local account.
93 ResolvingDomainAccount=Resolving {0} in the {1} domain.
94 ResolvingDomainAccountWithTrust=Resolving {0} with domain trust.
95 DomainCredentialsRequired=Credentials are required to resolve the domain account {0}.
96 UnableToResolveAccount=Unable to resolve account '{0}'. Failed with message: {1} (error code={2})
100 Import-Module "$PSScriptRoot\..\RunAsHelper.psm1"
102 Import-LocalizedData LocalizedData -FileName MSFT_GroupResource.strings.psd1
104 if (-not (IsNanoServer))
106 Add-Type -AssemblyName 'System.DirectoryServices.AccountManagement'
111 The Get-TargetResource cmdlet.
113 function Get-TargetResource
117 [parameter(Mandatory = $true)]
118 [ValidateNotNullOrEmpty()]
122 [System.Management.Automation.PSCredential]
128 return Get-TargetResourceOnNanoServer @PSBoundParameters
132 return Get-TargetResourceOnFullSKU @PSBoundParameters
139 The Set-TargetResource cmdlet.
141 function Set-TargetResource
143 [CmdletBinding(SupportsShouldProcess=$true)]
146 [parameter(Mandatory = $true)]
147 [ValidateNotNullOrEmpty()]
151 [ValidateSet("Present", "Absent")]
167 [ValidateNotNullOrEmpty()]
168 [System.Management.Automation.PSCredential]
174 Set-TargetResourceOnNanoServer @PSBoundParameters
178 Set-TargetResourceOnFullSKU @PSBoundParameters
184 The Test-TargetResource cmdlet is used to validate if the resource is in a state as expected in the instance document.
186 function Test-TargetResource
190 [parameter(Mandatory = $true)]
191 [ValidateNotNullOrEmpty()]
195 [ValidateSet("Present", "Absent")]
211 [ValidateNotNullOrEmpty()]
212 [System.Management.Automation.PSCredential]
218 return Test-TargetResourceOnNanoServer @PSBoundParameters
222 return Test-TargetResourceOnFullSKU @PSBoundParameters
228 The Get-TargetResource cmdlet for Full SKU images.
230 function Get-TargetResourceOnFullSKU
234 [parameter(Mandatory = $true)]
235 [ValidateNotNullOrEmpty()]
239 [System.Management.Automation.PSCredential]
243 Set-StrictMode -Version Latest
245 ValidateGroupName -GroupName $GroupName
247 # store disposable objects in a list for cleanup later.
248 # This is needed for the case where utility functions need to create
249 # disposable objects (Principal and PrincipalContext) and the
250 # object's life time is longer than the function. The $disposables
251 # collection is passed to these functions for storing the disposable objects
252 # and this function disposes the contents prior to returning.
253 # See references to DisposeAll for details.
254 $disposables = New-Object System.Collections.ArrayList
256 # hash table of scope to PrincipalContext. This is used for
257 # cases where the group membership contains entries that span the machine
258 # and one or more domains. The hashtable's key is the machine or domain
259 # name (scope) while the value is the PrincipalContext to use to resolve accounts
260 # within the named scope.
261 $principalContexts = @{}
265 [System.DirectoryServices.AccountManagement.GroupPrincipal] $group = GetGroup -groupName $GroupName -principalContexts $principalContexts -disposables $disposables
268 $null = $disposables.Add($group)
270 # The group is found. Enumerate all group members.
271 $members = [String[]]@(EnumerateMembersOnFullSKU -Group $group -principalContexts $principalContexts -disposables $disposables -credential $Credential)
273 # Return all group properties and Ensure="Present".
275 GroupName = $group.Name;
277 Description = $group.Description;
278 Members = [System.String[]] $members;
284 # The group is not found. Return Ensure=Absent.
286 GroupName = $GroupName;
292 DisposeAll $disposables
298 The Set-TargetResource cmdlet for Full SKU images.
300 function Set-TargetResourceOnFullSKU
302 [CmdletBinding(SupportsShouldProcess=$true)]
305 [parameter(Mandatory = $true)]
306 [ValidateNotNullOrEmpty()]
310 [ValidateSet("Present", "Absent")]
326 [ValidateNotNullOrEmpty()]
327 [System.Management.Automation.PSCredential]
331 Set-StrictMode -Version Latest
333 ValidateGroupName -GroupName $GroupName
335 # store disposable objects in a list for cleanup later.
336 # This is needed for the case where utility functions need to create
337 # disposable objects (Principal and PrincipalContext) and the
338 # object's life time is longer than the function. The $disposables
339 # collection is passed to these functions for storing the disposable objects
340 # and this function disposes the contents prior to returning.
341 # See references to DisposeAll for details.
342 $disposables = New-Object System.Collections.ArrayList
344 # hash table of scope to PrincipalContext. This is used for
345 # cases where the group membership contains entries that span the machine
346 # and one or more domains. The hashtable's key is the machine or domain
347 # name (scope) while the value is the PrincipalContext to use to resolve accounts
348 # within the named scope.
349 $principalContexts = @{}
353 # Try to find a group by its name.
354 [System.DirectoryServices.AccountManagement.GroupPrincipal] $group = GetGroup -groupName $GroupName -principalContexts $principalContexts -disposables $disposables
355 [bool] $groupExists = $false
357 if ($group -ne $null)
362 if($Ensure -eq "Present")
364 [System.DirectoryServices.AccountManagement.Principal[]] $membersToIncludePrincipals = $null
365 [System.DirectoryServices.AccountManagement.Principal[]] $membersToExcludePrincipals = $null
367 if ($group -ne $null)
369 $null = $disposables.Add($group)
372 # Ensure is set to "Present".
374 [bool] $whatIfShouldProcess = $true
375 [bool] $saveChanges = $false
377 if(-not $groupExists)
379 # A group does not exist. Check WhatIf for adding a group.
380 $whatIfShouldProcess = $pscmdlet.ShouldProcess(($LocalizedData.GroupWithName -f $GroupName), $LocalizedData.AddOperation)
384 # Check WhatIf for setting a group.
385 $whatIfShouldProcess = $pscmdlet.ShouldProcess(($LocalizedData.GroupWithName -f $GroupName), $LocalizedData.SetOperation)
388 if($whatIfShouldProcess)
390 if(-not $groupExists)
392 # NOTE: The PrincipalContext for the local machine is populated above in the call to GetGroup
393 $localPrincipalContext = $principalContexts[$env:COMPUTERNAME]
395 # The group with the provided name does not exist. Add a new group.
396 $group = New-Object System.DirectoryServices.AccountManagement.GroupPrincipal($localPrincipalContext)
397 $null = $disposables.Add($group)
399 $group.Name = $GroupName
403 # Set group properties.
405 if($PSBoundParameters.ContainsKey('Description') -and ((-not $groupExists) -or ($Description -ne $group.Description)))
407 $group.Description = $Description
411 # NOTE: Group members can be updated in two ways..
412 # 1: Supplying the Members parameter - this causes the membership to be replaced with the members defined in Members.
413 # NOTE: If Members is empty, the group membership is cleared.
414 # 2: Providing MembersToInclude and/or MembersToExclude - this adds/removes members from the list.
415 # If Members is mutually exclusive with MembersToInclude and MembersToExclude
416 # If Members is not defined then MembersToInclude or MembersToExclude must contain at least one entry.
418 if($PSBoundParameters.ContainsKey('Members'))
420 if($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude'))
422 # If Members are provided, Include and Exclude are not allowed.
423 ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersPlusIncludeOrExcludeConflict" -ErrorMessage ($LocalizedData.MembersAndIncludeExcludeConflict -f "Members","MembersToInclude","MembersToExclude")
426 if($Members -eq $null)
428 ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersIsNull" -ErrorMessage ($LocalizedData.MembersIsNull -f "Members","MembersToInclude","MembersToExclude")
431 if ($Members.Count -eq 0)
433 $group.Members.Clear()
438 # Remove duplicate names as strings.
439 $Members = [String[]]@(RemoveDuplicates -Members $Members)
441 # Resolve the names to actual principal objects.
442 [System.DirectoryServices.AccountManagement.Principal[]]$expectedPrincipals = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $Members
444 if ($expectedPrincipals.Length -gt 0)
446 $group.Members.Clear()
447 # Set the contents of the group
448 if ((AddGroupMembers -Group $group -Principals $expectedPrincipals) -eq $true)
455 #ISSUE: Is an empty $Members parameter valid?
456 ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_MembersEmpty" -ErrorMessage ($LocalizedData.MembersIsEmpty)
462 [System.DirectoryServices.AccountManagement.Principal[]] $membersToIncludePrincipals = $null
463 [System.DirectoryServices.AccountManagement.Principal[]] $membersToExcludePrincipals = $null
465 if($PSBoundParameters.ContainsKey('MembersToInclude'))
467 $MembersToInclude = [String[]]@(RemoveDuplicates -Members $MembersToInclude)
469 # Resolve the names to actual principal objects.
470 $membersToIncludePrincipals = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $MembersToInclude
473 if($PSBoundParameters.ContainsKey('MembersToExclude'))
475 $MembersToExclude = [String[]]@(RemoveDuplicates -Members $MembersToExclude)
477 # Resolve the names to actual principal objects.
478 $membersToExcludePrincipals = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $MembersToExclude
481 if($membersToIncludePrincipals -ne $null -and $membersToExcludePrincipals -ne $null)
483 # Both MembersToInclude and MembersToExlude were provided. Check if they have common principals.
484 foreach($includePrincipal in $membersToIncludePrincipals)
486 foreach($excludePrincipal in $membersToExcludePrincipals)
488 if($includePrincipal -eq $excludePrincipal)
490 ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_IncludeAndExcludeConflict" -ErrorMessage ($LocalizedData.IncludeAndExcludeConflict -f $includePrincipal.SamAccountName,"MembersToInclude", "MembersToExclude")
494 if ($membersToIncludePrincipals.Length -eq 0 -and $membersToExcludePrincipals.Length -eq 0)
496 ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_EmptyIncludeAndExclude" -ErrorMessage ($LocalizedData.IncludeAndExcludeAreEmpty)
501 if ((RemoveGroupMembers -Group $group -Principals $membersToExcludePrincipals) -eq $true)
506 if ((AddGroupMembers -Group $group -Principals $membersToIncludePrincipals) -eq $true)
516 # Send an operation success verbose message.
519 Write-Verbose -Message ($LocalizedData.GroupUpdated -f $GroupName)
523 Write-Verbose -Message ($LocalizedData.GroupCreated -f $GroupName)
528 Write-Verbose -Message ($LocalizedData.NoConfigurationRequired -f $GroupName)
534 # Ensure is set to "Absent".
535 if($groupExists -eq $true)
538 if($pscmdlet.ShouldProcess(($LocalizedData.GroupWithName -f $GroupName), $LocalizedData.RemoveOperation))
540 # Remove the group by the provided name.
541 # NOTE: Don't add to $disposables since Delete also disposes.
543 Write-Verbose -Message ($LocalizedData.GroupRemoved -f $GroupName)
547 $null = $disposables.Add($group)
552 Write-Verbose -Message ($LocalizedData.NoConfigurationRequiredGroupDoesNotExist -f $GroupName)
558 DisposeAll $disposables
564 The Test-TargetResource cmdlet for Full SKU images is used to validate if the resource is in a state as expected in the instance document.
566 function Test-TargetResourceOnFullSKU
570 [parameter(Mandatory = $true)]
571 [ValidateNotNullOrEmpty()]
575 [ValidateSet("Present", "Absent")]
591 [ValidateNotNullOrEmpty()]
592 [System.Management.Automation.PSCredential]
596 Set-StrictMode -Version Latest
598 ValidateGroupName -GroupName $GroupName
600 # store disposable objects in a list for cleanup later.
601 # This is needed for the case where utility functions need to create
602 # disposable objects (Principal and PrincipalContext) and the
603 # object's life time is longer than the function. The $disposables
604 # collection is passed to these functions for storing the disposable objects
605 # and this function disposes the contents prior to returning.
606 # See references to DisposeAll for details.
607 $disposables = New-Object System.Collections.ArrayList
609 # hash table of scope to PrincipalContext. This is used for
610 # cases where the group membership contains entries that span the machine
611 # and one or more domains. The hashtable's key is the machine or domain
612 # name (scope) while the value is the PrincipalContext to use to resolve accounts
613 # within the named scope.
614 $principalContexts = @{}
618 [System.DirectoryServices.AccountManagement.GroupPrincipal] $group = GetGroup -groupName $GroupName -principalContexts $principalContexts -disposables $disposables
621 # A group with the provided name does not exist.
622 Write-Log -Message ($LocalizedData.GroupDoesNotExist -f $GroupName)
624 if($Ensure -eq "Absent")
633 $null = $disposables.Add($group)
635 # A group with the provided name exists.
636 Write-Log -Message ($LocalizedData.GroupExists -f $GroupName)
638 # Validate separate properties.
639 if($Ensure -eq "Absent")
641 Write-Log -Message ($LocalizedData.PropertyMismatch -f "Ensure", "Absent", "Present")
642 return $false # The Ensure property does not match. Return $false
645 if($PSBoundParameters.ContainsKey('GroupName') -and $GroupName -ne $group.SamAccountName -and $GroupName -ne $group.Sid.Value)
647 return $false # The Name property does not match. Return $false
650 if($PSBoundParameters.ContainsKey('Description') -and $Description -ne $group.Description)
652 Write-Log -Message ($LocalizedData.PropertyMismatch -f "Description", $Description, $group.Description)
653 return $false # The Description property does not match. Return $false
656 if($PSBoundParameters.ContainsKey('Members'))
658 if($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude'))
660 # If Members are provided, Include and Exclude are not allowed.
661 ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersPlusIncludeOrExcludeConflict" -ErrorMessage ($LocalizedData.MembersAndIncludeExcludeConflict -f "Members","MembersToInclude","MembersToExclude")
664 if($Members -eq $null)
666 ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersIsNull" -ErrorMessage ($LocalizedData.MembersIsNull -f "Members","MembersToInclude","MembersToExclude")
669 if ($Members.Count -eq 0)
671 if ($group.Members.Count -eq 0)
682 # Remove duplicate names as strings.
683 $Members = [String[]]@(RemoveDuplicates -Members $Members)
685 # Resolve the names to actual principal objects.
686 [System.DirectoryServices.AccountManagement.Principal[]] $expectedMembers = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $Members
688 if($expectedMembers.Length -ne $group.Members.Count)
690 Write-Log -Message ($LocalizedData.MembersNumberMismatch -f "Members", $expectedMembers.Length, $group.Members.Count)
691 return $false; # The number of provided unique group members is different from the number of actual group members. Return $false.
694 [System.DirectoryServices.AccountManagement.Principal[]] $actualMembers = ResolveGroupMembersToPrincipals -group $group -principalContexts $principalContexts -disposables $disposables -credential $Credential
696 # Compare two members lists.
697 foreach ($expectedMember in $expectedMembers)
701 foreach($groupMember in $actualMembers)
703 if($expectedMember -eq $groupMember)
712 Write-Log -Message ($LocalizedData.MembersMemberMismatch -f $expectedMember.SamAccountName, "Members", $group.SamAccountName)
713 return $false # At least one element does not have a match. Return $false
720 [System.DirectoryServices.AccountManagement.Principal[]] $actualMembers = ResolveGroupMembersToPrincipals -group $group -principalContexts $principalContexts -disposables $disposables -credential $Credential
722 if($PSBoundParameters.ContainsKey('MembersToInclude'))
724 $MembersToInclude = [String[]]@(RemoveDuplicates -Members $MembersToInclude)
726 # Resolve the names to actual principal objects.
727 [System.DirectoryServices.AccountManagement.Principal[]] $membersToIncludePrincipals = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $MembersToInclude
729 # Check if every element in $membersToIncludePrincipals has a match in $group.Members.
730 # Compare two members lists.
731 foreach($expectedMember in $membersToIncludePrincipals)
735 foreach($groupMember in $actualMembers)
737 if($expectedMember -eq $groupMember)
746 Write-Log -Message ($LocalizedData.MembersMemberMismatch -f $expectedMember.SamAccountName, "MembersToInclude", $group.SamAccountName)
747 return $false # At least one element from $MembersToInclude does not have a match. Return $false
752 if($PSBoundParameters.ContainsKey('MembersToExclude'))
754 $MembersToExclude = [String[]]@(RemoveDuplicates -Members $MembersToExclude);
756 # Resolve the names to actual principal objects.
757 [System.DirectoryServices.AccountManagement.Principal[]] $membersToExcludePrincipals = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $MembersToExclude
759 foreach($expectedMember in $membersToExcludePrincipals)
761 foreach($groupMember in $actualMembers)
763 if($expectedMember -eq $groupMember)
765 Write-Log -Message ($LocalizedData.MemberToExcludeMatch -f $expectedMember.SamAccountName, "MembersToExclude", $group.SamAccountName)
766 return $false # At least one element from $MembersToExclude has a match. Return $false
775 DisposeAll $disposables
778 # All properties match. Return $true.
784 The Get-TargetResource cmdlet for Nano Server images.
786 function Get-TargetResourceOnNanoServer
790 [parameter(Mandatory = $true)]
791 [ValidateNotNullOrEmpty()]
795 [System.Management.Automation.PSCredential]
799 Set-StrictMode -Version Latest
801 ValidateGroupName -GroupName $GroupName
805 [Microsoft.PowerShell.Commands.LocalGroup] $group = Get-LocalGroup -Name $GroupName -ErrorAction Stop
807 catch [System.Exception]
809 if ($_.CategoryInfo.Reason -eq 'GroupNotFoundException')
811 # The group is not found. Return Ensure=Absent.
813 GroupName = $GroupName;
817 Throw-TerminatingError -ErrorRecord $_
820 # The group is found. Enumerate all group members.
821 $members = [String[]](EnumerateMembersOnNanoServer -Group $group)
823 # Return all group properties and Ensure="Present".
825 GroupName = $group.Name;
827 Description = $group.Description;
828 Members = [System.String[]] $members;
836 The Set-TargetResource cmdlet for Nano Server images.
838 function Set-TargetResourceOnNanoServer
840 [CmdletBinding(SupportsShouldProcess=$true)]
843 [parameter(Mandatory = $true)]
844 [ValidateNotNullOrEmpty()]
848 [ValidateSet("Present", "Absent")]
864 [ValidateNotNullOrEmpty()]
865 [System.Management.Automation.PSCredential]
869 Set-StrictMode -Version Latest
871 ValidateGroupName -GroupName $GroupName
873 # Try to find a group by its name.
874 [bool] $groupExists = $false
877 [Microsoft.PowerShell.Commands.LocalGroup] $group = Get-LocalGroup -Name $GroupName -ErrorAction Stop
880 catch [System.Exception]
882 if ($_.CategoryInfo.Reason -eq 'GroupNotFoundException')
884 # A group with the provided name does not exist.
885 Write-Log -Message ($LocalizedData.GroupDoesNotExist -f $GroupName)
889 Throw-TerminatingError -ErrorRecord $_
893 if($Ensure -eq "Present")
895 # Ensure is set to "Present".
896 if(-not $groupExists)
898 # The group with the provided name does not exist. Add a new group.
899 New-LocalGroup -Name $GroupName
900 Write-Verbose -Message ($LocalizedData.GroupCreated -f $GroupName)
903 # Set group properties.
905 if($PSBoundParameters.ContainsKey('Description') -and ((-not $groupExists) -or ($Description -ne $group.Description)))
907 Set-LocalGroup -Name $GroupName -Description $Description
910 # NOTE: Group members can be updated in two ways..
911 # 1: Supplying the Members parameter - this causes the membership to be replaced with the members defined in Members.
912 # NOTE: If Members is empty, the group membership is cleared.
913 # 2: Providing MembersToInclude and/or MembersToExclude - this adds/removes members from the list.
914 # If Members is mutually exclusive with MembersToInclude and MembersToExclude
915 # If Members is not defined then MembersToInclude or MembersToExclude must contain at least one entry.
917 if($PSBoundParameters.ContainsKey('Members'))
919 if($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude'))
921 # If Members are provided, Include and Exclude are not allowed.
922 ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersPlusIncludeOrExcludeConflict" -ErrorMessage ($LocalizedData.MembersAndIncludeExcludeConflict -f "Members","MembersToInclude","MembersToExclude")
925 if($Members -eq $null)
927 ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersIsNull" -ErrorMessage ($LocalizedData.MembersIsNull -f "Members","MembersToInclude","MembersToExclude")
930 # Remove duplicate names as strings.
931 $ExpectedMembers = [String[]]@(RemoveDuplicates -Members $Members)
933 if ($ExpectedMembers.Length -gt 0)
935 # Get current members
936 $CurrentMembers = EnumerateMembersOnNanoServer -Group $group
938 # Remove the current members of the group
939 Remove-LocalGroupMember -Group $GroupName -Member $CurrentMembers
941 # Add the list of expected members to the group
942 Add-LocalGroupMember -Group $GroupName -Member $ExpectedMembers
946 ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_MembersEmpty" -ErrorMessage ($LocalizedData.MembersIsEmpty)
951 if($PSBoundParameters.ContainsKey('MembersToInclude'))
953 $MembersToInclude = [String[]]@(RemoveDuplicates -Members $MembersToInclude)
956 if($PSBoundParameters.ContainsKey('MembersToExclude'))
958 $MembersToExclude = [String[]]@(RemoveDuplicates -Members $MembersToExclude)
961 if($PSBoundParameters.ContainsKey('MembersToInclude') -and $PSBoundParameters.ContainsKey('MembersToExclude'))
963 # Both MembersToInclude and MembersToExlude were provided. Check if they have common principals.
964 foreach($includeMember in $MembersToInclude)
966 foreach($excludeMember in $MembersToExclude)
968 if($includeMember -eq $excludeMember)
970 ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_IncludeAndExcludeConflict" -ErrorMessage ($LocalizedData.IncludeAndExcludeConflict -f $includeMember ,"MembersToInclude", "MembersToExclude")
974 if ($MembersToInclude.Length -eq 0 -and $MembersToExclude.Length -eq 0)
976 ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_EmptyIncludeAndExclude" -ErrorMessage ($LocalizedData.IncludeAndExcludeAreEmpty)
980 if($PSBoundParameters.ContainsKey('MembersToInclude'))
982 foreach($includeMember in $MembersToInclude)
986 Add-LocalGroupMember -Group $GroupName -Member $includeMember -ErrorAction Stop
988 catch [System.Exception]
990 if ($_.CategoryInfo.Reason -ne 'MemberExistsException')
998 if($PSBoundParameters.ContainsKey('MembersToExclude'))
1000 foreach($excludeMember in $MembersToExclude)
1004 Remove-LocalGroupMember -Group $GroupName -Member $excludeMember -ErrorAction Stop
1006 catch [System.Exception]
1008 if ($_.CategoryInfo.Reason -ne 'MemberNotFoundException')
1010 Throw-TerminatingError -ErrorRecord $_
1019 # Ensure is set to "Absent".
1020 if($groupExists -eq $true)
1022 # The group exists. Remove the group by the provided name.
1023 Remove-LocalGroup -Name $GroupName
1024 Write-Verbose -Message ($LocalizedData.GroupRemoved -f $GroupName)
1028 Write-Verbose -Message ($LocalizedData.NoConfigurationRequiredGroupDoesNotExist -f $GroupName)
1035 The Test-TargetResource cmdlet for Nano Server images is used to validate if the resource is in a state as expected in the instance document.
1037 function Test-TargetResourceOnNanoServer
1041 [parameter(Mandatory = $true)]
1042 [ValidateNotNullOrEmpty()]
1046 [ValidateSet("Present", "Absent")]
1048 $Ensure = "Present",
1062 [ValidateNotNullOrEmpty()]
1063 [System.Management.Automation.PSCredential]
1067 Set-StrictMode -Version Latest
1069 ValidateGroupName -GroupName $GroupName
1073 [Microsoft.PowerShell.Commands.LocalGroup] $group = Get-LocalGroup -Name $GroupName -ErrorAction Stop
1075 catch [System.Exception]
1077 if ($_.CategoryInfo.Reason -eq 'GroupNotFoundException')
1079 # A group with the provided name does not exist.
1080 Write-Log -Message ($LocalizedData.GroupDoesNotExist -f $GroupName)
1082 if($Ensure -eq "Absent")
1091 Throw-TerminatingError -ErrorRecord $_
1094 # A group with the provided name exists.
1095 Write-Log -Message ($LocalizedData.GroupExists -f $GroupName)
1097 # Validate separate properties.
1098 if($Ensure -eq "Absent")
1100 Write-Log -Message ($LocalizedData.PropertyMismatch -f "Ensure", "Absent", "Present")
1101 return $false # The Ensure property does not match. Return $false
1104 if($PSBoundParameters.ContainsKey('Description') -and $Description -ne $group.Description)
1106 Write-Log -Message ($LocalizedData.PropertyMismatch -f "Description", $Description, $group.Description)
1107 return $false # The Description property does not match. Return $false
1110 if($PSBoundParameters.ContainsKey('Members'))
1112 Write-Verbose "Testing members..."
1113 if($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude'))
1115 # If Members are provided, Include and Exclude are not allowed.
1116 ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersPlusIncludeOrExcludeConflict" -ErrorMessage ($LocalizedData.MembersAndIncludeExcludeConflict -f "Members","MembersToInclude","MembersToExclude")
1119 if($Members -eq $null)
1121 ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersIsNull" -ErrorMessage ($LocalizedData.MembersIsNull -f "Members","MembersToInclude","MembersToExclude")
1124 # Remove duplicate names as strings.
1125 $ExpectedMembers = [String[]]@(RemoveDuplicates -Members $Members)
1127 # Get current members
1128 $CurrentMembers = EnumerateMembersOnNanoServer -Group $group
1130 if($ExpectedMembers.Length -ne $CurrentMembers.Length)
1132 Write-Log -Message ($LocalizedData.MembersNumberMismatch -f "Members", $ExpectedMembers.Length, $CurrentMembers.Length)
1133 return $false; # The number of provided unique group members is different from the number of actual group members. Return $false.
1136 # Compare two members lists.
1137 foreach ($ExpectedMember in $ExpectedMembers)
1139 $matchFound = $false
1141 foreach($groupMember in $CurrentMembers)
1143 if($ExpectedMember -eq $groupMember)
1150 if(-not $matchFound)
1152 Write-Log -Message ($LocalizedData.MembersMemberMismatch -f $expectedMember, "Members", $group.Name)
1153 return $false # At least one element does not have a match. Return $false
1159 # Get current members
1160 $CurrentMembers = EnumerateMembersOnNanoServer -Group $group
1162 if($PSBoundParameters.ContainsKey('MembersToInclude'))
1164 $MembersToInclude = [String[]]@(RemoveDuplicates -Members $MembersToInclude)
1166 # Check if every element in $membersToIncludePrincipals has a match in $group.Members.
1167 # Compare two members lists.
1168 foreach($expectedMember in $MembersToInclude)
1170 $matchFound = $false
1172 foreach($groupMember in $CurrentMembers)
1174 if($expectedMember -eq $groupMember)
1181 if(-not $matchFound)
1183 Write-Log -Message ($LocalizedData.MembersMemberMismatch -f $expectedMember, "MembersToInclude", $group.Name)
1184 return $false # At least one element from $MembersToInclude does not have a match. Return $false
1189 if($PSBoundParameters.ContainsKey('MembersToExclude'))
1191 $MembersToExclude = [String[]]@(RemoveDuplicates -Members $MembersToExclude);
1193 foreach($expectedMember in $MembersToExclude)
1195 foreach($groupMember in $CurrentMembers)
1197 if($expectedMember -eq $groupMember)
1199 Write-Log -Message ($LocalizedData.MemberToExcludeMatch -f $expectedMember, "MembersToExclude", $group.Name)
1200 return $false # At least one element from $MembersToExclude has a match. Return $false
1207 # All properties match. Return $true.
1211 function RemoveDuplicates
1215 [System.String[]] $Members
1218 Set-StrictMode -Version Latest
1221 for([int] $sourceIndex = 0 ; $sourceIndex -lt $Members.Count; $sourceIndex++)
1223 $matchFound = $false
1224 for([int] $matchIndex = 0; $matchIndex -lt $destIndex; $matchIndex++)
1226 if($Members[$sourceIndex] -eq $Members[$matchIndex])
1228 # A duplicate is found. Discard the duplicate.
1236 $Members[$destIndex++] = $Members[$sourceIndex].ToLowerInvariant();
1240 # Create the output array.
1241 $destination = New-Object System.String[] -ArgumentList $destIndex
1243 # Copy only distinct elements from the original array to the destination array.
1244 [System.Array]::Copy($Members, $destination, $destIndex);
1246 if ($destIndex -gt 0)
1250 return [System.String[]]@()
1253 function EnumerateMembersOnNanoServer
1255 [OutputType([System.String[]])]
1258 [parameter(Mandatory = $true)]
1260 [Microsoft.PowerShell.Commands.LocalGroup]
1264 Set-StrictMode -Version Latest
1265 [System.Collections.ArrayList] $members = New-Object System.Collections.ArrayList
1267 # Get the group members.
1268 $groupmembers = Get-LocalGroupMember -Group $Group
1270 foreach($member in $groupmembers)
1272 if ($member.PrincipalSource -eq "Local")
1274 $null = $members.Add($member.Name.Substring($member.Name.IndexOf("\")+1))
1278 Write-Verbose "$($member.Name) is not a local user (PrincipalSource = $($member.PrincipalSource))"
1282 if ($members.Count -gt 0)
1284 return $members.ToArray()
1286 return ,([System.String[]]@())
1289 function EnumerateMembersOnFullSKU
1291 [OutputType([System.String[]])]
1294 [parameter(Mandatory = $true)]
1296 [System.DirectoryServices.AccountManagement.GroupPrincipal]
1299 [Parameter(Mandatory = $true)]
1303 [Parameter(Mandatory = $true)]
1305 [System.Collections.ArrayList]
1308 [System.Net.NetworkCredential]
1312 Set-StrictMode -Version Latest
1313 [System.Collections.ArrayList] $members = New-Object System.Collections.ArrayList
1315 # Get the group members as Principal objects.
1316 [System.DirectoryServices.AccountManagement.Principal[]] $principals = ResolveGroupMembersToPrincipals -group $group -principalContexts $principalContexts -disposables $disposables -credential $credential
1318 foreach($principal in $principals)
1320 if($principal.ContextType -eq [System.DirectoryServices.AccountManagement.ContextType]::Domain)
1322 # Select only the first part of the full domain name.
1323 [String]$domainName = $principal.Context.Name;
1324 [int] $separatorIndex = $domainName.IndexOf('.')
1325 if ($separatorIndex -ne -1)
1327 $domainName = $domainName.Substring(0, $separatorIndex)
1330 if($principal.StructuralObjectClass -eq "computer")
1332 $null = $members.Add($domainName+'\'+$principal.Name)
1336 $null = $members.Add($domainName+'\'+$principal.SamAccountName)
1341 $null = $members.Add($principal.Name)
1345 return $members.ToArray()
1350 Resolves the members of a group to Principal instances.
1352 function ResolveGroupMembersToPrincipals
1354 [OutputType([System.DirectoryServices.AccountManagement.Principal[]])]
1357 [parameter(Mandatory = $true)]
1359 [System.DirectoryServices.AccountManagement.GroupPrincipal]
1362 [Parameter(Mandatory = $true)]
1366 [Parameter(Mandatory = $true)]
1368 [System.Collections.ArrayList]
1371 [System.Net.NetworkCredential]
1374 Set-StrictMode -Version latest
1376 [System.Collections.ArrayList] $principals = New-Object System.Collections.ArrayList
1378 # NOTE: This logic enumerates the group members using the underlying DirectoryEntry API.
1379 # The reason this is needed is due to the fact that enumerating the group
1380 # members as principal instances causes a resolve to occur. Since there is no
1381 # facility for passing credentials to perform the resolution, any members that
1382 # cannot be resolved using the current user will fail; such as when the DSC
1383 # Group resource runs as system. Dropping down to the underyling API
1384 # allows us to access the account's SID which can then be used to
1385 # resolve the associated principal using explicit credentials.
1386 [System.DirectoryServices.DirectoryEntry] $groupDe = $group.GetUnderlyingObject()
1388 $enum = $groupDe.Invoke("Members")
1389 foreach ($item in $enum)
1391 [string] $scope = $null
1392 [string] $accountName = $null
1393 [string] $machineName = $env:COMPUTERNAME
1394 [System.DirectoryServices.AccountManagement.Principal] $principal = $null
1396 # extract the objectSid from the underlying DirectoryEntry
1397 [System.DirectoryServices.DirectoryEntry] $entry = New-Object System.DirectoryServices.DirectoryEntry($item)
1398 [byte[]] $sidBytes = $entry.Properties["objectSid"].Value
1400 $null = $disposables.Add($entry)
1402 [string[]] $parts = $entry.Path.Split("/")
1404 if ($parts.Count -eq 4)
1406 # parsing WinNT://domainname/accountname
1407 # or WinNT://machinename/accountname
1409 $accountName = $parts[3]
1411 elseif ($parts.Count -eq 5)
1413 # parsing WinNT://domainname/machinename/accountname
1415 $accountName = $parts[4]
1419 # the account is stale either becuase it was deleted or
1420 # the machine was moved to a new domain without removing
1421 # the domain members from the group.
1422 # If we consider this a fatal error, the group is no longer
1423 # managable by the DSC resource. Writing a warning allows
1424 # the operation to complete while leaving the stale member in the
1426 Write-Warning -Message ($LocalizedData.MemberNotValid -f $entry.Path)
1431 [bool] $isLocalMachine = [System.String]::CompareOrdinal($scope, $machineName) -eq 0
1433 $principalContext = GetPrincipalContext -principalContexts $principalContexts -disposables $disposables -scope $scope -credential $credential
1435 # if local machine qualified, get the PrincipalContext for the local machine
1436 if ($isLocalMachine -eq $true)
1438 Write-Verbose -Message ($LocalizedData.ResolvingLocalAccount -f $accountName)
1440 # the account is domain qualified - credentials required to resolve it.
1441 elseif ($credential -ne $null -or $principalContext -ne $null)
1443 Write-Verbose -Message ($LocalizedData.ResolvingDomainAccount -f $scope, $accountName)
1447 # The provided name is not scoped to the local machine and no credentials were provided.
1448 # This is an unsupported use case; credentials are required to resolve off-box.
1449 ThrowInvalidArgumentError -ErrorId "PrincipalNotFoundNoCredential" -ErrorMessage ($LocalizedData.DomainCredentialsRequired -f $accountName)
1452 # create a sid to enable comparison againt the expected member's sid.
1453 [System.Security.Principal.SecurityIdentifier] $sid = New-Object System.Security.Principal.SecurityIdentifier($sidBytes, 0)
1455 $principal = ResolveSidToPrincipal -principalContext $principalContext -sid $sid -isLocalMachineQualified $isLocalMachine
1456 $null = $principals.Add($principal)
1457 $null = $disposables.Add($principal)
1460 return $principals.ToArray()
1465 Resolves an array of object names to Principal instances.
1467 function ResolveNamesToPrincipals
1471 [Parameter(Mandatory = $true)]
1473 [String[]] $objectNames,
1475 [Parameter(Mandatory = $true)]
1477 [System.Collections.ArrayList] $disposables,
1479 [Parameter(Mandatory = $true)]
1483 [System.Net.NetworkCredential]
1486 Set-StrictMode -Version Latest
1488 [System.Collections.ArrayList] $principals = New-Object System.Collections.ArrayList
1491 foreach($objectName in $objectNames)
1493 $principal = ResolveNameToPrincipal -principalContexts $principalContexts -credential $credential -disposables $disposables -objectName $objectName
1494 if ($principal -ne $null)
1496 [string] $key = $null
1497 # handle duplicate entries
1498 if ($principal.ContextType -eq [System.DirectoryServices.AccountManagement.ContextType]::Domain)
1500 $key = $principal.DistinguishedName
1504 $key = $principal.SamAccountName
1506 if ($keys.ContainsKey($key) -eq $false)
1508 $keys.Add($key, $null)
1509 $null = $principals.Add($principal)
1515 return $principals.ToArray()
1520 resolves an object name to a Principal
1522 function ResolveNameToPrincipal
1524 [OutputType([System.DirectoryServices.AccountManagement.Principal])]
1527 [Parameter(Mandatory = $true)]
1531 [Parameter(Mandatory = $true)]
1535 [Parameter(Mandatory = $true)]
1536 [ValidateNotNullOrEmpty()]
1537 [string] $objectName,
1539 [System.Net.NetworkCredential]
1543 Set-StrictMode -Version Latest
1545 [string] $accountName = $null
1547 # the scope of the the object name when in the form of scope\name, UPN, or DN
1548 [string] $scope = Parse-Scope -fullName $objectName -accountName ([ref] $accountName)
1550 # check for an object qualified to the local machine
1551 [bool] $isLocalMachine = IsLocalMachine $scope
1553 [System.DirectoryServices.AccountManagement.PrincipalContext] $principalContext = $null
1554 [bool] $UseDomainTrust = $false
1556 # if local machine qualified, get the PrincipalContext for the local machine
1557 if ($isLocalMachine -eq $true)
1559 Write-Verbose -Message ($LocalizedData.ResolvingLocalAccount -f $objectName)
1561 # the account is domain qualified - credentials provided to resolve it.
1562 elseif ($credential -ne $null)
1564 Write-Verbose -Message ($LocalizedData.ResolvingDomainAccount -f $ObjectName, $scope)
1566 # no credentials provided to resolve account name, so try with domain trust
1569 # The provided name is not scoped to the local machine and no credentials were provided.
1570 # If the object is a domain qualified name, we can try to resolve the user with domain trust, if setup.
1571 $UseDomainTrust = $true
1572 Write-Verbose -Message ($LocalizedData.ResolvingDomainAccountWithTrust -f $objectName)
1575 # Get a PrincipalContext to use to resolve the object
1576 $principalContext = GetPrincipalContext -principalContexts $principalContexts -disposables $disposables -scope $scope -credential $credential
1578 if ($UseDomainTrust)
1580 # When using domain trust, we use the object name to resolve. Object name can be in different formats such as a domain
1581 # qualified name, UPN, or a distinguished name for the scope
1582 $account = $objectName
1586 $account = $accountName
1591 [System.DirectoryServices.AccountManagement.Principal] $principal = [System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($principalContext, $account)
1593 catch [System.Runtime.InteropServices.COMException]
1595 ThrowInvalidArgumentError -ErrorId "PrincipalNotFound" -ErrorMessage ( $LocalizedData.UnableToResolveAccount -f $objectName, $_.Exception.Message, $_.Exception.HResult )
1598 if ($principal -eq $null)
1600 [string] $errorId = $null
1601 if ($isLocalMachine)
1603 $errorId = "PrincipalNotFound_LocalMachine"
1607 $errorId = "PrincipalNotFound_ProvidedCredential"
1610 ThrowInvalidArgumentError -ErrorId $errorId -ErrorMessage ($LocalizedData.CouldNotFindPrincipal -f $objectName)
1618 Resolves a SID to a principal
1620 function ResolveSidToPrincipal
1622 [OutputType([System.DirectoryServices.AccountManagement.Principal])]
1625 [Parameter(Mandatory = $true)]
1627 [System.DirectoryServices.AccountManagement.PrincipalContext] $principalContext,
1629 [Parameter(Mandatory = $true)]
1631 [System.Security.Principal.SecurityIdentifier] $sid,
1633 [Parameter(Mandatory = $true)]
1634 [bool] $isLocalMachineQualified
1636 Set-StrictMode -Version Latest
1638 [string] $sidValue = $Sid.Value
1640 # Try to find a matching principal.
1641 $principal = [System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($principalContext, [System.DirectoryServices.AccountManagement.IdentityType]::Sid, $sidValue)
1643 if ($principal -eq $null)
1645 [string] $errorId = $null
1646 if ($isLocalMachineQualified)
1648 $errorId = "PrincipalNotFound_LocalMachine"
1652 $errorId = "PrincipalNotFound_ProvidedCredential"
1655 ThrowInvalidArgumentError -ErrorId $errorId -ErrorMessage ($LocalizedData.CouldNotFindPrincipal -f $sid.ToString())
1663 Gets a PrincipalContext to use to resolve an object in the specified $scope
1664 $principalContexts is a hashtable of scope to PrincipalContext. This is used to
1665 cache PrincipalContext instances for cases where it is used multiple times.
1666 disposables is an array of disposable objects. When a new PrincipalContext is
1667 created, it is added to the disposable list as well as the hashtable.
1668 $credential is used when a new PrincipalContext needs to be created with explicit
1669 credentials to a target domain
1671 function GetPrincipalContext
1673 [OutputType([System.DirectoryServices.AccountManagement.PrincipalContext])]
1676 [Parameter(Mandatory = $true)]
1680 [Parameter(Mandatory = $true)]
1684 [Parameter(Mandatory = $true)]
1685 [ValidateNotNullOrEmpty()]
1688 [System.Net.NetworkCredential]
1692 # The PrincipalContext to use to resolve the account
1693 [System.DirectoryServices.AccountManagement.PrincipalContext] $principalContext = $null
1695 # check for an object qualified to the local machine
1696 [bool] $isLocalMachine = [System.String]::Compare($env:COMPUTERNAME, $scope) -eq 0
1698 if ($isLocalMachine)
1700 # check for a cached PrincipalContext for the local machine.
1701 if ($principalContexts.ContainsKey($env:COMPUTERNAME))
1703 $principalContext = $principalContexts[$env:COMPUTERNAME]
1707 # Create a PrincipalContext for the local machine
1708 $principalContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Machine)
1710 # Cache the PrincipalContext for this scope for subsequent calls.
1711 $principalContexts.Add($env:COMPUTERNAME, $principalContext)
1712 $null = $disposables.Add($principalContext)
1715 elseif ($principalContexts.ContainsKey($scope))
1717 $principalContext = $principalContexts[$scope]
1719 elseif ($credential -ne $null)
1721 # Create a PrincipalContext targeing $scope using the network credentials that were passed in.
1723 $name = [System.String]::Format("{0}\{1}", $credential.Domain, $credential.UserName)
1724 $principalContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Domain, $scope, $name, $credential.Password)
1726 # Cache the PrincipalContext for this scope for subsequent calls.
1727 $principalContexts.Add($scope, $principalContext)
1728 $null = $disposables.Add($principalContext)
1732 # Get a PrincipalContext for the current user in the target domain (even for local System account).
1733 $principalContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Domain, $scope)
1735 # Cache the PrincipalContext for this scope for subsequent calls.
1736 $principalContexts.Add($scope, $principalContext)
1737 $null = $disposables.Add($principalContext)
1740 return $principalContext
1746 Adds the entries defined in $Principals from $Group.Members.
1747 Returns $true if the members changed (i.e., members were added);
1748 otherwise, $false if all of the entries in $Principals were already present
1750 #>function AddGroupMembers
1752 [OutputType([bool])]
1755 [System.DirectoryServices.AccountManagement.GroupPrincipal] $Group,
1757 [System.DirectoryServices.AccountManagement.Principal[]] $Principals
1759 Set-StrictMode -Version Latest
1760 [bool] $updated = $false
1762 if ($Principals -ne $null)
1764 # Make changes to the group.
1765 foreach($principal in $Principals)
1767 if ($group.Members.Contains($principal))
1771 $group.Members.Add($principal)
1772 # indicate a change was made to $Group.Members
1781 Removes the entries defined in $Principals from $Group.Members.
1782 Returns $true if the members changed (i.e., members were removed);
1783 otherwise, $false if none of the entries in $Principals were present
1786 function RemoveGroupMembers
1788 [OutputType([bool])]
1791 [System.DirectoryServices.AccountManagement.GroupPrincipal] $Group,
1793 [System.DirectoryServices.AccountManagement.Principal[]] $Principals
1795 Set-StrictMode -Version Latest
1796 [bool] $updated = $false
1798 if ($Principals -ne $null)
1800 # Make changes to the group.
1801 foreach($principal in $Principals)
1803 if ($group.Members.Remove($principal) -eq $true)
1805 # indicated a change was made to the members.
1818 Determines if a scope represents the current machine.
1820 function IsLocalMachine
1822 [OutputType([bool])]
1825 [Parameter(Mandatory=$true)]
1826 [ValidateNotNullOrEmpty()]
1830 Set-StrictMode -Version latest
1837 if ($scope -eq $env:COMPUTERNAME)
1842 if ($scope -eq "localhost")
1847 if ($scope.Contains("."))
1849 if ($scope -eq "127.0.0.1")
1854 # Determine if we have an ip address that matches an ip address on one of the
1856 # NOTE: This is likely overkill; consider removing it.
1857 $items = @(Get-WmiObject Win32_NetworkAdapterConfiguration)
1858 foreach ($item in $items)
1860 if ($item.IPaddress -ne $null)
1862 foreach ($addr in $item.IPaddress)
1864 if ($addr -eq $scope)
1877 Determines if a specified domain is the same as the domain defined in a PrincipalContext.
1878 This is used to determine if a new connection (PrincipalContext) needs to be created.
1881 This method uses simple string compare and simple parsing to perform the match and
1882 should only be used to determine if a new connection is required since some
1883 comparisons may returned $false due to formatting differences.
1885 function IsSameDomain
1887 [OutputType([bool])]
1890 [Parameter(Mandatory=$true)]
1892 [System.DirectoryServices.AccountManagement.PrincipalContext] $principalContext,
1894 [Parameter(Mandatory=$true)]
1898 Set-StrictMode -Version latest
1900 # Compare against Name of $principalContext - typically the undecorated domain name
1901 [bool] $isSameDomain = [System.String]::Compare($domain, $principalContext.Name, [System.StringComparison]::OrdinalIgnoreCase) -eq 0
1903 if ($isSameDomain -eq $false)
1905 $dotIndex = $principalContext.ConnectedServer.IndexOf(".")
1906 if ($dotIndex -ne -1)
1908 $principalDomain = $principalContext.ConnectedServer.Substring($dotIndex + 1)
1909 $isSameDomain = [System.String]::Compare($domain, $principalDomain, [System.StringComparison]::OrdinalIgnoreCase) -eq 0
1913 return $isSameDomain
1918 Parses various object name formats to extract the machine or domain scope.
1920 The returned $scope is used to determine where to perform the resolution,
1921 the local machine or a target domain while $accountName is the name
1922 of the account to resolve.
1924 The following details the formats that are handled as well as how the
1925 values are determined.
1927 Domain qualified names (domainname\username)
1929 The value is split on the first '\' character with the left hand side
1930 returned as the scope and the right hand side is returned as the account name.
1932 UPN: (username@domainname)
1934 The value is split on the first '@' character with the left hand side
1935 returned as the account name and the right hand side returned
1940 The value at the first occurance of "DC=" is used to extract the unqualified
1941 domain name. The incoming string is returned, as is, for the account
1944 Unqualified account names:
1946 The incoming string is returned as the account name and the local
1947 machine name is returned as the scope. Note that values that do
1948 not fall into the above categories are interpreted as unqualified
1952 ResolveNameToPrincipal will fail if a machine name is specified
1953 as domainname\machinename. It will succeed if the machine name
1954 is specified as the SAM name (domainname\machinename$) or
1955 as the unqualified machine name.
1956 Parse-Scope splits the scope and account name to avoid
1959 function Parse-Scope
1961 [OutputType([string])]
1964 [Parameter(Mandatory=$true)]
1965 [ValidateNotNullOrEmpty()]
1969 [Parameter(Mandatory=$true)]
1970 [AllowEmptyString()]
1974 Set-StrictMode -Version latest
1976 # assume no scope is defined or $fullName is a DistinguishedName
1977 $accountName.Value = $fullName
1979 # parse domain or machine qualified account name
1980 [int] $separatorIndex = $fullName.IndexOf("\")
1981 if ($separatorIndex -ne -1)
1983 $scope = $fullName.Substring(0, $separatorIndex)
1984 if (IsLocalMachine $scope)
1986 $scope = $env:COMPUTERNAME
1988 $accountName.Value = $fullName.Substring($separatorIndex+1)
1992 # parse UPN for the scope
1993 $separatorIndex = $fullName.IndexOf("@")
1994 if ($separatorIndex -ne -1)
1996 $scope = $fullName.Substring($separatorIndex + 1)
1997 $accountName.Value = $fullName.Substring(0,$separatorIndex)
2001 # parse distinguished name for the scope
2002 $separatorIndex = $fullName.IndexOf("DC=", [System.StringComparison]::OrdinalIgnoreCase)
2003 if ($separatorIndex -ne -1)
2005 # NOTE: For distinguished name formats, the DistinguishedName is
2006 # returned as the account name. See the initialization of $accountName
2008 $startIndex = $separatorIndex + 3
2009 $endIndex = $fullName.IndexOf(",", $startIndex)
2010 if ($endIndex -gt $startIndex)
2012 $length = $endIndex - $separatorIndex - 3
2013 $scope = $fullName.Substring($startIndex, $length)
2017 return $env:COMPUTERNAME
2022 Disposes the contents of an array list containing IDisposable objects.
2028 [Parameter(Mandatory = $true)]
2030 [AllowEmptyCollection()]
2031 [System.Collections.ArrayList]
2034 Set-StrictMode -Version latest
2036 if ($disposables.Count -gt 0)
2038 foreach ($disposable in $disposables)
2040 if ($disposable -is [System.IDisposable])
2042 $disposable.Dispose()
2050 Gets a local windows group
2053 The returned value is NOT added to the $disposables list.
2057 [OutputType([System.DirectoryServices.AccountManagement.GroupPrincipal])]
2060 [Parameter(Mandatory = $true)]
2061 [ValidateNotNullOrEmpty()]
2062 [string] $groupName,
2064 [Parameter(Mandatory = $true)]
2066 [System.Collections.ArrayList]
2067 [AllowEmptyCollection()]
2070 [Parameter(Mandatory = $true)]
2071 [AllowEmptyCollection()]
2075 [System.DirectoryServices.AccountManagement.PrincipalContext] $principalContext = GetPrincipalContext -principalContexts $principalContexts -disposables $disposables -scope $env:COMPUTERNAME -credential $null
2076 [System.DirectoryServices.AccountManagement.GroupPrincipal] $group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($principalContext, $groupName)
2077 # NOTE: $group is not automatically added to $disposables because the caller
2078 # may need to call $group.Delete() which also disposes it.
2084 Validates the Group name for invalid characters.
2086 function ValidateGroupName
2090 [parameter(Mandatory = $true)]
2091 [ValidateNotNullOrEmpty()]
2096 # Check if the name consists of only periods and/or white spaces.
2097 [bool] $wrongName = $true
2099 for($i = 0; $i -lt $GroupName.Length; $i++)
2101 if(-not [Char]::IsWhiteSpace($GroupName, $i) -and $GroupName[$i] -ne '.')
2108 $invalidChars = @('\','/','"','[',']',':','|','<','>','+','=',';',',','?','*','@')
2112 ThrowInvalidArgumentError -ErrorId "GroupNameHasOnlyWhiteSpacesAndDots" -ErrorMessage ($LocalizedData.InvalidGroupName -f $GroupName, [string]::Join(" ", $invalidChars))
2115 if($GroupName.IndexOfAny($invalidChars) -ne -1)
2117 ThrowInvalidArgumentError -ErrorId "GroupNameHasInvalidCharachter" -ErrorMessage ($LocalizedData.InvalidGroupName -f $GroupName, [string]::Join(" ", $invalidChars))
2123 Throws an argument error.
2125 function ThrowInvalidArgumentError
2131 [parameter(Mandatory = $true)]
2132 [ValidateNotNullOrEmpty()]
2136 [parameter(Mandatory = $true)]
2137 [ValidateNotNullOrEmpty()]
2142 $errorCategory=[System.Management.Automation.ErrorCategory]::InvalidArgument
2143 $exception = New-Object System.ArgumentException $ErrorMessage;
2144 $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $ErrorId, $errorCategory, $null
2148 Function Throw-TerminatingError
2152 [System.Management.Automation.ErrorRecord] $ErrorRecord,
2153 [string] $ExceptionType
2156 $exception = new-object "System.InvalidOperationException" $Message,$ErrorRecord.Exception
2157 $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"MachineStateIncorrect","InvalidOperation",$null
2163 Writes either to Verbose or ShouldProcess channel.
2167 [CmdletBinding(SupportsShouldProcess=$true)]
2170 [parameter(Mandatory = $true)]
2171 [ValidateNotNullOrEmpty()]
2176 if ($PSCmdlet.ShouldProcess($Message, $null, $null))
2178 Write-Verbose $Message
2184 Export-ModuleMember -function Get-TargetResource, Set-TargetResource, Test-TargetResource