]> insang Git - newton-cn_pos.git/blob
71686db722a23cd8f56baa0b9b040b11abfa5cea
[newton-cn_pos.git] /
1 <#
2 Implementatation notes
3
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.
12
13 Managing PrincipalContext instances.
14     To use the AccountManagement APIs to connect to the local machine or a domain,
15     a PrincipalContext is needed.
16
17     For the local groups and users, a PrincipalContext reflecting the current user
18     can be created.
19
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
22     domain.
23
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.
30
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.
40
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
47     to resolve.
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.
52
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.
61
62 #>
63
64 # A global variable that contains localized messages.
65 data LocalizedData
66 {
67 # culture="en-US"
68 ConvertFrom-StringData @'
69 GroupWithName=Group: {0}
70 RemoveOperation=Remove
71 AddOperation=Add
72 SetOperation=Set
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})
97 '@
98 }
99
100 Import-Module "$PSScriptRoot\..\RunAsHelper.psm1"
101
102 Import-LocalizedData LocalizedData -FileName MSFT_GroupResource.strings.psd1
103
104 if (-not (IsNanoServer))
105 {
106     Add-Type -AssemblyName 'System.DirectoryServices.AccountManagement'
107 }
108
109 <#
110 .Synopsis
111 The Get-TargetResource cmdlet.
112 #>
113 function Get-TargetResource
114 {
115     param
116     (
117         [parameter(Mandatory = $true)]
118         [ValidateNotNullOrEmpty()]
119         [System.String]
120         $GroupName,
121
122         [System.Management.Automation.PSCredential]
123         $Credential = $null
124     )
125
126     if (IsNanoServer)
127     {
128         return Get-TargetResourceOnNanoServer @PSBoundParameters
129     }
130     else
131     {
132         return Get-TargetResourceOnFullSKU @PSBoundParameters
133     }
134
135 }
136
137 <#
138 .Synopsis
139 The Set-TargetResource cmdlet.
140 #>
141 function Set-TargetResource
142 {
143     [CmdletBinding(SupportsShouldProcess=$true)]
144     param
145     (
146         [parameter(Mandatory = $true)]
147         [ValidateNotNullOrEmpty()]
148         [System.String]
149         $GroupName,
150
151         [ValidateSet("Present", "Absent")]
152         [System.String]
153         $Ensure = "Present",
154
155         [System.String]
156         $Description,
157
158         [System.String[]]
159         $Members,
160
161         [System.String[]]
162         $MembersToInclude,
163
164         [System.String[]]
165         $MembersToExclude,
166
167         [ValidateNotNullOrEmpty()]
168         [System.Management.Automation.PSCredential]
169         $Credential
170     )
171
172     if (IsNanoServer)
173     {
174         Set-TargetResourceOnNanoServer @PSBoundParameters
175     }
176     else
177     {
178         Set-TargetResourceOnFullSKU @PSBoundParameters
179     }
180 }
181
182 <#
183 .Synopsis
184 The Test-TargetResource cmdlet is used to validate if the resource is in a state as expected in the instance document.
185 #>
186 function Test-TargetResource
187 {
188     param
189     (
190         [parameter(Mandatory = $true)]
191         [ValidateNotNullOrEmpty()]
192         [System.String]
193         $GroupName,
194
195         [ValidateSet("Present", "Absent")]
196         [System.String]
197         $Ensure = "Present",
198
199         [System.String]
200         $Description,
201
202         [System.String[]]
203         $Members,
204
205         [System.String[]]
206         $MembersToInclude,
207
208         [System.String[]]
209         $MembersToExclude,
210
211         [ValidateNotNullOrEmpty()]
212         [System.Management.Automation.PSCredential]
213         $Credential
214     )
215
216     if (IsNanoServer)
217     {
218         return Test-TargetResourceOnNanoServer @PSBoundParameters
219     }
220     else
221     {
222         return Test-TargetResourceOnFullSKU @PSBoundParameters
223     }
224 }
225
226 <#
227 .Synopsis
228 The Get-TargetResource cmdlet for Full SKU images.
229 #>
230 function Get-TargetResourceOnFullSKU
231 {
232     param
233     (
234         [parameter(Mandatory = $true)]
235         [ValidateNotNullOrEmpty()]
236         [System.String]
237         $GroupName,
238
239         [System.Management.Automation.PSCredential]
240         $Credential = $null
241     )
242
243     Set-StrictMode -Version Latest
244
245     ValidateGroupName -GroupName $GroupName
246
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
255
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 = @{}
262
263     try
264     {
265         [System.DirectoryServices.AccountManagement.GroupPrincipal] $group = GetGroup -groupName $GroupName -principalContexts $principalContexts -disposables $disposables
266         if($group -ne $null)
267         {
268             $null = $disposables.Add($group)
269
270             # The group is found. Enumerate all group members.
271             $members = [String[]]@(EnumerateMembersOnFullSKU -Group $group -principalContexts $principalContexts -disposables $disposables -credential $Credential)
272
273             # Return all group properties and Ensure="Present".
274             $returnValue = @{
275                                 GroupName = $group.Name;
276                                 Ensure = "Present";
277                                 Description = $group.Description;
278                                 Members = [System.String[]] $members;
279                             }
280
281             return $returnValue
282         }
283
284         # The group is not found. Return Ensure=Absent.
285         return @{
286                     GroupName = $GroupName;
287                     Ensure = "Absent";
288                 }
289     }
290     finally
291     {
292         DisposeAll $disposables
293     }
294 }
295
296 <#
297 .Synopsis
298 The Set-TargetResource cmdlet for Full SKU images.
299 #>
300 function Set-TargetResourceOnFullSKU
301 {
302     [CmdletBinding(SupportsShouldProcess=$true)]
303     param
304     (
305         [parameter(Mandatory = $true)]
306         [ValidateNotNullOrEmpty()]
307         [System.String]
308         $GroupName,
309
310         [ValidateSet("Present", "Absent")]
311         [System.String]
312         $Ensure = "Present",
313
314         [System.String]
315         $Description,
316
317         [System.String[]]
318         $Members,
319
320         [System.String[]]
321         $MembersToInclude,
322
323         [System.String[]]
324         $MembersToExclude,
325
326         [ValidateNotNullOrEmpty()]
327         [System.Management.Automation.PSCredential]
328         $Credential
329     )
330
331     Set-StrictMode -Version Latest
332
333     ValidateGroupName -GroupName $GroupName
334
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
343
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 = @{}
350
351     try
352     {
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
356
357         if ($group -ne $null)
358         {
359             $groupExists = $true
360         }
361
362         if($Ensure -eq "Present")
363         {
364             [System.DirectoryServices.AccountManagement.Principal[]] $membersToIncludePrincipals = $null
365             [System.DirectoryServices.AccountManagement.Principal[]] $membersToExcludePrincipals = $null
366
367             if ($group -ne $null)
368             {
369                 $null = $disposables.Add($group)
370             }
371
372             # Ensure is set to "Present".
373
374             [bool] $whatIfShouldProcess = $true
375             [bool] $saveChanges = $false
376
377             if(-not $groupExists)
378             {
379                 # A group does not exist. Check WhatIf for adding a group.
380                 $whatIfShouldProcess = $pscmdlet.ShouldProcess(($LocalizedData.GroupWithName -f $GroupName), $LocalizedData.AddOperation)
381             }
382             else
383             {
384                 # Check WhatIf for setting a group.
385                 $whatIfShouldProcess = $pscmdlet.ShouldProcess(($LocalizedData.GroupWithName -f $GroupName), $LocalizedData.SetOperation)
386             }
387
388             if($whatIfShouldProcess)
389             {
390                 if(-not $groupExists)
391                 {
392                     # NOTE: The PrincipalContext for the local machine is populated above in the call to GetGroup
393                     $localPrincipalContext = $principalContexts[$env:COMPUTERNAME]
394
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)
398
399                     $group.Name = $GroupName
400                     $saveChanges = $true
401                 }
402
403                 # Set group properties.
404
405                 if($PSBoundParameters.ContainsKey('Description') -and ((-not $groupExists) -or ($Description -ne $group.Description)))
406                 {
407                     $group.Description = $Description
408                     $saveChanges = $true
409                 }
410
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.
417
418                 if($PSBoundParameters.ContainsKey('Members'))
419                 {
420                     if($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude'))
421                     {
422                         # If Members are provided, Include and Exclude are not allowed.
423                         ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersPlusIncludeOrExcludeConflict" -ErrorMessage ($LocalizedData.MembersAndIncludeExcludeConflict -f "Members","MembersToInclude","MembersToExclude")
424                     }
425
426                     if($Members -eq $null)
427                     {
428                         ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersIsNull" -ErrorMessage ($LocalizedData.MembersIsNull -f "Members","MembersToInclude","MembersToExclude")
429                     }
430
431                     if ($Members.Count -eq 0)
432                     {
433                         $group.Members.Clear()
434                         $saveChanges = $true
435                     }
436                     else
437                     {
438                         # Remove duplicate names as strings.
439                         $Members = [String[]]@(RemoveDuplicates -Members $Members)
440
441                         # Resolve the names to actual principal objects.
442                         [System.DirectoryServices.AccountManagement.Principal[]]$expectedPrincipals = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $Members
443
444                         if ($expectedPrincipals.Length -gt 0)
445                         {
446                             $group.Members.Clear()
447                             # Set the contents of the group
448                             if ((AddGroupMembers -Group $group -Principals $expectedPrincipals) -eq $true)
449                             {
450                                 $saveChanges = $true
451                             }
452                         }
453                         else
454                         {
455                             #ISSUE: Is an empty $Members parameter valid?
456                             ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_MembersEmpty" -ErrorMessage ($LocalizedData.MembersIsEmpty)
457                         }
458                     }
459                 }
460                 else
461                 {
462                     [System.DirectoryServices.AccountManagement.Principal[]] $membersToIncludePrincipals = $null
463                     [System.DirectoryServices.AccountManagement.Principal[]] $membersToExcludePrincipals = $null
464
465                     if($PSBoundParameters.ContainsKey('MembersToInclude'))
466                     {
467                         $MembersToInclude = [String[]]@(RemoveDuplicates -Members $MembersToInclude)
468
469                         # Resolve the names to actual principal objects.
470                         $membersToIncludePrincipals = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $MembersToInclude
471                     }
472
473                     if($PSBoundParameters.ContainsKey('MembersToExclude'))
474                     {
475                         $MembersToExclude = [String[]]@(RemoveDuplicates -Members $MembersToExclude)
476
477                         # Resolve the names to actual principal objects.
478                         $membersToExcludePrincipals = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $MembersToExclude
479                     }
480
481                     if($membersToIncludePrincipals -ne $null -and $membersToExcludePrincipals -ne $null)
482                     {
483                         # Both MembersToInclude and MembersToExlude were provided. Check if they have common principals.
484                         foreach($includePrincipal in $membersToIncludePrincipals)
485                         {
486                             foreach($excludePrincipal in $membersToExcludePrincipals)
487                             {
488                                 if($includePrincipal -eq $excludePrincipal)
489                                 {
490                                     ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_IncludeAndExcludeConflict" -ErrorMessage ($LocalizedData.IncludeAndExcludeConflict -f $includePrincipal.SamAccountName,"MembersToInclude", "MembersToExclude")
491                                 }
492                             }
493                         }
494                         if ($membersToIncludePrincipals.Length -eq 0 -and $membersToExcludePrincipals.Length -eq 0)
495                         {
496                             ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_EmptyIncludeAndExclude" -ErrorMessage ($LocalizedData.IncludeAndExcludeAreEmpty)
497                         }
498                     }
499
500
501                     if ((RemoveGroupMembers -Group $group -Principals $membersToExcludePrincipals) -eq $true)
502                     {
503                         $saveChanges = $true
504                     }
505
506                     if ((AddGroupMembers -Group $group -Principals $membersToIncludePrincipals) -eq $true)
507                     {
508                         $saveChanges = $true
509                     }
510                 }
511
512                 if($saveChanges)
513                 {
514                     $group.Save()
515
516                     # Send an operation success verbose message.
517                     if($groupExists)
518                     {
519                         Write-Verbose -Message ($LocalizedData.GroupUpdated -f $GroupName)
520                     }
521                     else
522                     {
523                         Write-Verbose -Message ($LocalizedData.GroupCreated -f $GroupName)
524                     }
525                 }
526                 else
527                 {
528                     Write-Verbose -Message ($LocalizedData.NoConfigurationRequired -f $GroupName)
529                 }
530             }
531         }
532         else
533         {
534             # Ensure is set to "Absent".
535             if($groupExists -eq $true)
536             {
537                 # The group exists.
538                 if($pscmdlet.ShouldProcess(($LocalizedData.GroupWithName -f $GroupName), $LocalizedData.RemoveOperation))
539                 {
540                     # Remove the group by the provided name.
541                     # NOTE: Don't add to $disposables since Delete also disposes.
542                     $group.Delete()
543                     Write-Verbose -Message ($LocalizedData.GroupRemoved -f $GroupName)
544                 }
545                 else
546                 {
547                     $null = $disposables.Add($group)
548                 }
549             }
550             else
551             {
552                 Write-Verbose -Message ($LocalizedData.NoConfigurationRequiredGroupDoesNotExist -f $GroupName)
553             }
554         }
555     }
556     finally
557     {
558         DisposeAll $disposables
559     }
560 }
561
562 <#
563 .Synopsis
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.
565 #>
566 function Test-TargetResourceOnFullSKU
567 {
568     param
569     (
570         [parameter(Mandatory = $true)]
571         [ValidateNotNullOrEmpty()]
572         [System.String]
573         $GroupName,
574
575         [ValidateSet("Present", "Absent")]
576         [System.String]
577         $Ensure = "Present",
578
579         [System.String]
580         $Description,
581
582         [System.String[]]
583         $Members,
584
585         [System.String[]]
586         $MembersToInclude,
587
588         [System.String[]]
589         $MembersToExclude,
590
591         [ValidateNotNullOrEmpty()]
592         [System.Management.Automation.PSCredential]
593         $Credential
594     )
595
596     Set-StrictMode -Version Latest
597
598     ValidateGroupName -GroupName $GroupName
599
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
608
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 = @{}
615
616     try
617     {
618         [System.DirectoryServices.AccountManagement.GroupPrincipal] $group = GetGroup -groupName $GroupName -principalContexts $principalContexts -disposables  $disposables
619         if($group -eq $null)
620         {
621             # A group with the provided name does not exist.
622             Write-Log -Message ($LocalizedData.GroupDoesNotExist -f $GroupName)
623
624             if($Ensure -eq "Absent")
625             {
626                 return $true
627             }
628             else
629             {
630                 return $false
631             }
632         }
633         $null = $disposables.Add($group)
634
635         # A group with the provided name exists.
636         Write-Log -Message ($LocalizedData.GroupExists -f $GroupName)
637
638         # Validate separate properties.
639         if($Ensure -eq "Absent")
640         {
641             Write-Log -Message ($LocalizedData.PropertyMismatch -f "Ensure", "Absent", "Present")
642             return $false # The Ensure property does not match. Return $false
643         }
644
645         if($PSBoundParameters.ContainsKey('GroupName') -and $GroupName -ne $group.SamAccountName -and $GroupName -ne $group.Sid.Value)
646         {
647             return $false # The Name property does not match. Return $false
648         }
649
650         if($PSBoundParameters.ContainsKey('Description') -and $Description -ne $group.Description)
651         {
652             Write-Log -Message ($LocalizedData.PropertyMismatch -f "Description", $Description, $group.Description)
653             return $false # The Description property does not match. Return $false
654         }
655
656         if($PSBoundParameters.ContainsKey('Members'))
657         {
658             if($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude'))
659             {
660                 # If Members are provided, Include and Exclude are not allowed.
661                 ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersPlusIncludeOrExcludeConflict" -ErrorMessage ($LocalizedData.MembersAndIncludeExcludeConflict -f "Members","MembersToInclude","MembersToExclude")
662             }
663
664             if($Members -eq $null)
665             {
666                 ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersIsNull" -ErrorMessage ($LocalizedData.MembersIsNull -f "Members","MembersToInclude","MembersToExclude")
667             }
668
669             if ($Members.Count -eq 0)
670             {
671                 if ($group.Members.Count -eq 0)
672                 {
673                     return $true
674                 }
675                 else
676                 {
677                     return $false
678                 }
679             }
680             else
681             {
682                 # Remove duplicate names as strings.
683                 $Members = [String[]]@(RemoveDuplicates -Members $Members)
684
685                 # Resolve the names to actual principal objects.
686                 [System.DirectoryServices.AccountManagement.Principal[]] $expectedMembers = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $Members
687
688                 if($expectedMembers.Length -ne $group.Members.Count)
689                 {
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.
692                 }
693
694                 [System.DirectoryServices.AccountManagement.Principal[]] $actualMembers = ResolveGroupMembersToPrincipals -group $group -principalContexts $principalContexts -disposables $disposables -credential $Credential
695
696                 # Compare two members lists.
697                 foreach ($expectedMember in $expectedMembers)
698                 {
699                     $matchFound = $false
700
701                     foreach($groupMember in $actualMembers)
702                     {
703                         if($expectedMember -eq $groupMember)
704                         {
705                             $matchFound = $true
706                             break;
707                         }
708                     }
709
710                     if(!$matchFound)
711                     {
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
714                     }
715                 }
716             }
717         }
718         else
719         {
720             [System.DirectoryServices.AccountManagement.Principal[]] $actualMembers = ResolveGroupMembersToPrincipals -group $group -principalContexts $principalContexts -disposables $disposables -credential $Credential
721
722             if($PSBoundParameters.ContainsKey('MembersToInclude'))
723             {
724                 $MembersToInclude = [String[]]@(RemoveDuplicates -Members $MembersToInclude)
725
726                 # Resolve the names to actual principal objects.
727                 [System.DirectoryServices.AccountManagement.Principal[]] $membersToIncludePrincipals = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $MembersToInclude
728
729                 # Check if every element in $membersToIncludePrincipals has a match in $group.Members.
730                 # Compare two members lists.
731                 foreach($expectedMember in $membersToIncludePrincipals)
732                 {
733                     $matchFound = $false
734
735                     foreach($groupMember in $actualMembers)
736                     {
737                         if($expectedMember -eq $groupMember)
738                         {
739                             $matchFound = $true
740                             break
741                         }
742                     }
743
744                     if(!$matchFound)
745                     {
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
748                     }
749                 }
750             }
751
752             if($PSBoundParameters.ContainsKey('MembersToExclude'))
753             {
754                 $MembersToExclude = [String[]]@(RemoveDuplicates -Members $MembersToExclude);
755
756                 # Resolve the names to actual principal objects.
757                 [System.DirectoryServices.AccountManagement.Principal[]] $membersToExcludePrincipals = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $MembersToExclude
758
759                 foreach($expectedMember in $membersToExcludePrincipals)
760                 {
761                     foreach($groupMember in $actualMembers)
762                     {
763                         if($expectedMember -eq $groupMember)
764                         {
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
767                         }
768                     }
769                 }
770             }
771         }
772     }
773     finally
774     {
775         DisposeAll $disposables
776     }
777
778     # All properties match. Return $true.
779     return $true;
780 }
781
782 <#
783 .Synopsis
784 The Get-TargetResource cmdlet for Nano Server images.
785 #>
786 function Get-TargetResourceOnNanoServer
787 {
788     param
789     (
790         [parameter(Mandatory = $true)]
791         [ValidateNotNullOrEmpty()]
792         [System.String]
793         $GroupName,
794
795         [System.Management.Automation.PSCredential]
796         $Credential = $null
797     )
798
799     Set-StrictMode -Version Latest
800
801     ValidateGroupName -GroupName $GroupName
802
803     try
804     {
805         [Microsoft.PowerShell.Commands.LocalGroup] $group = Get-LocalGroup -Name $GroupName -ErrorAction Stop
806     }
807     catch [System.Exception]
808     {
809         if ($_.CategoryInfo.Reason -eq 'GroupNotFoundException')
810         {
811             # The group is not found. Return Ensure=Absent.
812             return @{
813                         GroupName = $GroupName;
814                         Ensure = "Absent";
815                     }
816         }
817         Throw-TerminatingError -ErrorRecord $_
818     }
819
820     # The group is found. Enumerate all group members.
821     $members = [String[]](EnumerateMembersOnNanoServer -Group $group)
822
823     # Return all group properties and Ensure="Present".
824     $returnValue = @{
825                         GroupName = $group.Name;
826                         Ensure = "Present";
827                         Description = $group.Description;
828                         Members = [System.String[]] $members;
829                     }
830     
831     return $returnValue
832 }
833
834 <#
835 .Synopsis
836 The Set-TargetResource cmdlet for Nano Server images.
837 #>
838 function Set-TargetResourceOnNanoServer
839 {
840     [CmdletBinding(SupportsShouldProcess=$true)]
841     param
842     (
843         [parameter(Mandatory = $true)]
844         [ValidateNotNullOrEmpty()]
845         [System.String]
846         $GroupName,
847
848         [ValidateSet("Present", "Absent")]
849         [System.String]
850         $Ensure = "Present",
851
852         [System.String]
853         $Description,
854
855         [System.String[]]
856         $Members,
857
858         [System.String[]]
859         $MembersToInclude,
860
861         [System.String[]]
862         $MembersToExclude,
863
864         [ValidateNotNullOrEmpty()]
865         [System.Management.Automation.PSCredential]
866         $Credential
867     )
868
869     Set-StrictMode -Version Latest
870
871     ValidateGroupName -GroupName $GroupName
872     
873     # Try to find a group by its name.
874     [bool] $groupExists = $false
875     try
876     {
877         [Microsoft.PowerShell.Commands.LocalGroup] $group = Get-LocalGroup -Name $GroupName -ErrorAction Stop
878         $groupExists = $true
879     }
880     catch [System.Exception]
881     {
882         if ($_.CategoryInfo.Reason -eq 'GroupNotFoundException')
883         {
884             # A group with the provided name does not exist.
885             Write-Log -Message ($LocalizedData.GroupDoesNotExist -f $GroupName)
886         }
887         else
888         {
889             Throw-TerminatingError -ErrorRecord $_
890         }
891     }
892
893     if($Ensure -eq "Present")
894     {
895         # Ensure is set to "Present".
896         if(-not $groupExists)
897         {
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)
901         }
902     
903         # Set group properties.
904         
905         if($PSBoundParameters.ContainsKey('Description') -and ((-not $groupExists) -or ($Description -ne $group.Description)))
906         {
907             Set-LocalGroup -Name $GroupName -Description $Description
908         }
909         
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.
916         
917         if($PSBoundParameters.ContainsKey('Members'))
918         {
919             if($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude'))
920             {
921                 # If Members are provided, Include and Exclude are not allowed.
922                 ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersPlusIncludeOrExcludeConflict" -ErrorMessage ($LocalizedData.MembersAndIncludeExcludeConflict -f "Members","MembersToInclude","MembersToExclude")
923             }
924         
925             if($Members -eq $null)
926             {
927                 ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersIsNull" -ErrorMessage ($LocalizedData.MembersIsNull -f "Members","MembersToInclude","MembersToExclude")
928             }
929         
930             # Remove duplicate names as strings.
931             $ExpectedMembers = [String[]]@(RemoveDuplicates -Members $Members)
932         
933             if ($ExpectedMembers.Length -gt 0)
934             {
935                 # Get current members
936                 $CurrentMembers = EnumerateMembersOnNanoServer -Group $group
937
938                 # Remove the current members of the group
939                 Remove-LocalGroupMember -Group $GroupName -Member $CurrentMembers
940
941                 # Add the list of expected members to the group
942                 Add-LocalGroupMember -Group $GroupName -Member $ExpectedMembers
943             }
944             else
945             {
946                 ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_MembersEmpty" -ErrorMessage ($LocalizedData.MembersIsEmpty)
947             }
948         }
949         else
950         {
951             if($PSBoundParameters.ContainsKey('MembersToInclude'))
952             {
953                 $MembersToInclude = [String[]]@(RemoveDuplicates -Members $MembersToInclude)
954             }
955        
956             if($PSBoundParameters.ContainsKey('MembersToExclude'))
957             {
958                 $MembersToExclude = [String[]]@(RemoveDuplicates -Members $MembersToExclude)
959             }
960        
961             if($PSBoundParameters.ContainsKey('MembersToInclude') -and $PSBoundParameters.ContainsKey('MembersToExclude'))
962             {
963                 # Both MembersToInclude and MembersToExlude were provided. Check if they have common principals.
964                 foreach($includeMember in $MembersToInclude)
965                 {
966                     foreach($excludeMember in $MembersToExclude)
967                     {
968                         if($includeMember -eq $excludeMember)
969                         {
970                             ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_IncludeAndExcludeConflict" -ErrorMessage ($LocalizedData.IncludeAndExcludeConflict -f $includeMember ,"MembersToInclude", "MembersToExclude")
971                         }
972                     }
973                 }
974                 if ($MembersToInclude.Length -eq 0 -and $MembersToExclude.Length -eq 0)
975                 {
976                     ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_EmptyIncludeAndExclude" -ErrorMessage ($LocalizedData.IncludeAndExcludeAreEmpty)
977                 }
978             }
979             
980             if($PSBoundParameters.ContainsKey('MembersToInclude'))
981             {
982                 foreach($includeMember in $MembersToInclude)
983                 {
984                     try
985                     {
986                         Add-LocalGroupMember -Group $GroupName -Member $includeMember -ErrorAction Stop
987                     }
988                     catch [System.Exception]
989                     {
990                         if ($_.CategoryInfo.Reason -ne 'MemberExistsException')
991                         {
992                             throw $_.Exception
993                         }
994                     }
995                 }
996             }
997        
998             if($PSBoundParameters.ContainsKey('MembersToExclude'))
999             {
1000                 foreach($excludeMember in $MembersToExclude)
1001                 {
1002                     try
1003                     {
1004                         Remove-LocalGroupMember -Group $GroupName -Member $excludeMember -ErrorAction Stop
1005                     }
1006                     catch [System.Exception]
1007                     {
1008                         if ($_.CategoryInfo.Reason -ne 'MemberNotFoundException')
1009                         {
1010                             Throw-TerminatingError -ErrorRecord $_
1011                         }
1012                     }
1013                 }
1014             }
1015         }
1016     }
1017     else
1018     {
1019         # Ensure is set to "Absent".
1020         if($groupExists -eq $true)
1021         {
1022             # The group exists. Remove the group by the provided name.
1023             Remove-LocalGroup -Name $GroupName
1024             Write-Verbose -Message ($LocalizedData.GroupRemoved -f $GroupName)
1025         }
1026         else
1027         {
1028             Write-Verbose -Message ($LocalizedData.NoConfigurationRequiredGroupDoesNotExist -f $GroupName)
1029         }
1030     }
1031 }
1032
1033 <#
1034 .Synopsis
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.
1036 #>
1037 function Test-TargetResourceOnNanoServer
1038 {
1039     param
1040     (
1041         [parameter(Mandatory = $true)]
1042         [ValidateNotNullOrEmpty()]
1043         [System.String]
1044         $GroupName,
1045
1046         [ValidateSet("Present", "Absent")]
1047         [System.String]
1048         $Ensure = "Present",
1049
1050         [System.String]
1051         $Description,
1052
1053         [System.String[]]
1054         $Members,
1055
1056         [System.String[]]
1057         $MembersToInclude,
1058
1059         [System.String[]]
1060         $MembersToExclude,
1061
1062         [ValidateNotNullOrEmpty()]
1063         [System.Management.Automation.PSCredential]
1064         $Credential
1065     )
1066
1067     Set-StrictMode -Version Latest
1068
1069     ValidateGroupName -GroupName $GroupName
1070
1071     try
1072     {
1073         [Microsoft.PowerShell.Commands.LocalGroup] $group = Get-LocalGroup -Name $GroupName -ErrorAction Stop
1074     }
1075     catch [System.Exception]
1076     {
1077         if ($_.CategoryInfo.Reason -eq 'GroupNotFoundException')
1078         {
1079             # A group with the provided name does not exist.
1080             Write-Log -Message ($LocalizedData.GroupDoesNotExist -f $GroupName)
1081         
1082             if($Ensure -eq "Absent")
1083             {
1084                 return $true
1085             }
1086             else
1087             {
1088                 return $false
1089             }
1090         }
1091         Throw-TerminatingError -ErrorRecord $_
1092     }
1093
1094     # A group with the provided name exists.
1095     Write-Log -Message ($LocalizedData.GroupExists -f $GroupName)
1096
1097     # Validate separate properties.
1098     if($Ensure -eq "Absent")
1099     {
1100         Write-Log -Message ($LocalizedData.PropertyMismatch -f "Ensure", "Absent", "Present")
1101         return $false # The Ensure property does not match. Return $false
1102     }
1103     
1104     if($PSBoundParameters.ContainsKey('Description') -and $Description -ne $group.Description)
1105     {
1106         Write-Log -Message ($LocalizedData.PropertyMismatch -f "Description", $Description, $group.Description)
1107         return $false # The Description property does not match. Return $false
1108     }
1109     
1110     if($PSBoundParameters.ContainsKey('Members'))
1111     {
1112         Write-Verbose "Testing members..."
1113         if($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude'))
1114         {
1115             # If Members are provided, Include and Exclude are not allowed.
1116             ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersPlusIncludeOrExcludeConflict" -ErrorMessage ($LocalizedData.MembersAndIncludeExcludeConflict -f "Members","MembersToInclude","MembersToExclude")
1117         }
1118     
1119         if($Members -eq $null)
1120         {
1121             ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersIsNull" -ErrorMessage ($LocalizedData.MembersIsNull -f "Members","MembersToInclude","MembersToExclude")
1122         }
1123     
1124         # Remove duplicate names as strings.
1125         $ExpectedMembers = [String[]]@(RemoveDuplicates -Members $Members)
1126
1127         # Get current members
1128         $CurrentMembers = EnumerateMembersOnNanoServer -Group $group
1129         
1130         if($ExpectedMembers.Length -ne $CurrentMembers.Length)
1131         {
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.
1134         }
1135     
1136         # Compare two members lists.
1137         foreach ($ExpectedMember in $ExpectedMembers)
1138         {
1139             $matchFound = $false
1140         
1141             foreach($groupMember in $CurrentMembers)
1142             {
1143                 if($ExpectedMember -eq $groupMember)
1144                 {
1145                     $matchFound = $true
1146                     break;
1147                 }
1148             }
1149         
1150             if(-not $matchFound)
1151             {
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
1154             }
1155         }
1156     }
1157     else
1158     {
1159         # Get current members
1160         $CurrentMembers = EnumerateMembersOnNanoServer -Group $group
1161
1162         if($PSBoundParameters.ContainsKey('MembersToInclude'))
1163         {
1164             $MembersToInclude = [String[]]@(RemoveDuplicates -Members $MembersToInclude)
1165     
1166             # Check if every element in $membersToIncludePrincipals has a match in $group.Members.
1167             # Compare two members lists.
1168             foreach($expectedMember in $MembersToInclude)
1169             {
1170                 $matchFound = $false
1171     
1172                 foreach($groupMember in $CurrentMembers)
1173                 {
1174                     if($expectedMember -eq $groupMember)
1175                     {
1176                         $matchFound = $true
1177                         break
1178                     }
1179                 }
1180     
1181                 if(-not $matchFound)
1182                 {
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
1185                 }
1186             }
1187         }
1188     
1189         if($PSBoundParameters.ContainsKey('MembersToExclude'))
1190         {
1191             $MembersToExclude = [String[]]@(RemoveDuplicates -Members $MembersToExclude);
1192     
1193             foreach($expectedMember in $MembersToExclude)
1194             {
1195                 foreach($groupMember in $CurrentMembers)
1196                 {
1197                     if($expectedMember -eq $groupMember)
1198                     {
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
1201                     }
1202                 }
1203             }
1204         }
1205     }
1206
1207     # All properties match. Return $true.
1208     return $true;
1209 }
1210
1211 function RemoveDuplicates
1212 {
1213     param
1214     (
1215         [System.String[]] $Members
1216     )
1217
1218     Set-StrictMode -Version Latest
1219
1220     $destIndex = 0;
1221     for([int] $sourceIndex = 0 ; $sourceIndex -lt $Members.Count; $sourceIndex++)
1222     {
1223         $matchFound = $false
1224         for([int] $matchIndex = 0; $matchIndex -lt $destIndex; $matchIndex++)
1225         {
1226             if($Members[$sourceIndex] -eq $Members[$matchIndex])
1227             {
1228                 # A duplicate is found. Discard the duplicate.
1229                 $matchFound = $true
1230                 continue
1231             }
1232         }
1233
1234         if(!$matchFound)
1235         {
1236             $Members[$destIndex++] = $Members[$sourceIndex].ToLowerInvariant();
1237         }
1238     }
1239
1240     # Create the output array.
1241     $destination = New-Object System.String[] -ArgumentList $destIndex
1242
1243     # Copy only distinct elements from the original array to the destination array.
1244     [System.Array]::Copy($Members, $destination, $destIndex);
1245
1246     if ($destIndex -gt 0)
1247     {
1248         return $destination
1249     }
1250     return [System.String[]]@()
1251 }
1252
1253 function EnumerateMembersOnNanoServer
1254 {
1255     [OutputType([System.String[]])]
1256     param
1257     (
1258         [parameter(Mandatory = $true)]
1259         [ValidateNotNull()]
1260         [Microsoft.PowerShell.Commands.LocalGroup]
1261         $Group
1262     )
1263
1264     Set-StrictMode -Version Latest
1265     [System.Collections.ArrayList] $members = New-Object System.Collections.ArrayList
1266
1267     # Get the group members.
1268     $groupmembers = Get-LocalGroupMember -Group $Group
1269
1270     foreach($member in $groupmembers)
1271     {
1272         if ($member.PrincipalSource -eq "Local")
1273         {
1274             $null = $members.Add($member.Name.Substring($member.Name.IndexOf("\")+1))
1275         }
1276         else
1277         {
1278             Write-Verbose "$($member.Name) is not a local user (PrincipalSource = $($member.PrincipalSource))"
1279         }
1280     }
1281
1282     if ($members.Count -gt 0)
1283     {
1284         return $members.ToArray()
1285     }
1286     return ,([System.String[]]@())
1287 }
1288
1289 function EnumerateMembersOnFullSKU
1290 {
1291     [OutputType([System.String[]])]
1292     param
1293     (
1294         [parameter(Mandatory = $true)]
1295         [ValidateNotNull()]
1296         [System.DirectoryServices.AccountManagement.GroupPrincipal]
1297         $group,
1298
1299         [Parameter(Mandatory = $true)]
1300         [ValidateNotNull()]
1301         $principalContexts,
1302
1303         [Parameter(Mandatory = $true)]
1304         [ValidateNotNull()]
1305         [System.Collections.ArrayList]
1306         $disposables,
1307
1308         [System.Net.NetworkCredential]
1309         $credential = $null
1310     )
1311
1312     Set-StrictMode -Version Latest
1313     [System.Collections.ArrayList] $members = New-Object System.Collections.ArrayList
1314
1315     # Get the group members as Principal objects.
1316     [System.DirectoryServices.AccountManagement.Principal[]] $principals = ResolveGroupMembersToPrincipals -group $group -principalContexts $principalContexts -disposables  $disposables -credential $credential
1317
1318     foreach($principal in $principals)
1319     {
1320         if($principal.ContextType -eq [System.DirectoryServices.AccountManagement.ContextType]::Domain)
1321         {
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)
1326             {
1327                 $domainName = $domainName.Substring(0, $separatorIndex)
1328             }
1329
1330             if($principal.StructuralObjectClass -eq "computer")
1331             {
1332                 $null = $members.Add($domainName+'\'+$principal.Name)
1333             }
1334             else
1335             {
1336                 $null = $members.Add($domainName+'\'+$principal.SamAccountName)
1337             }
1338         }
1339         else
1340         {
1341             $null = $members.Add($principal.Name)
1342         }
1343     }
1344
1345     return $members.ToArray()
1346 }
1347
1348 <#
1349 .Synopsis
1350     Resolves the members of a group to Principal instances.
1351 #>
1352 function ResolveGroupMembersToPrincipals
1353 {
1354     [OutputType([System.DirectoryServices.AccountManagement.Principal[]])]
1355     param
1356     (
1357         [parameter(Mandatory = $true)]
1358         [ValidateNotNull()]
1359         [System.DirectoryServices.AccountManagement.GroupPrincipal]
1360         $group,
1361
1362         [Parameter(Mandatory = $true)]
1363         [ValidateNotNull()]
1364         $principalContexts,
1365
1366         [Parameter(Mandatory = $true)]
1367         [ValidateNotNull()]
1368         [System.Collections.ArrayList]
1369         $disposables,
1370
1371         [System.Net.NetworkCredential]
1372         $credential = $null
1373     )
1374     Set-StrictMode -Version latest
1375
1376     [System.Collections.ArrayList] $principals = New-Object System.Collections.ArrayList
1377
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()
1387
1388     $enum = $groupDe.Invoke("Members")
1389     foreach ($item in $enum)
1390     {
1391         [string] $scope = $null
1392         [string] $accountName = $null
1393         [string] $machineName = $env:COMPUTERNAME
1394         [System.DirectoryServices.AccountManagement.Principal] $principal = $null
1395
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
1399
1400         $null = $disposables.Add($entry)
1401
1402         [string[]] $parts = $entry.Path.Split("/")
1403
1404         if ($parts.Count -eq 4)
1405         {
1406             # parsing WinNT://domainname/accountname
1407             # or WinNT://machinename/accountname
1408             $scope = $parts[2]
1409             $accountName = $parts[3]
1410         }
1411         elseif ($parts.Count -eq 5)
1412         {
1413             # parsing WinNT://domainname/machinename/accountname
1414             $scope = $parts[3]
1415             $accountName = $parts[4]
1416         }
1417         else
1418         {
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
1425             # group.
1426             Write-Warning -Message ($LocalizedData.MemberNotValid -f  $entry.Path)
1427             continue
1428         }
1429
1430
1431         [bool] $isLocalMachine = [System.String]::CompareOrdinal($scope, $machineName) -eq 0
1432
1433         $principalContext = GetPrincipalContext -principalContexts $principalContexts -disposables $disposables -scope $scope -credential $credential
1434
1435         # if local machine qualified, get the PrincipalContext for the local machine
1436         if ($isLocalMachine -eq $true)
1437         {
1438             Write-Verbose -Message ($LocalizedData.ResolvingLocalAccount -f $accountName)
1439         }
1440         # the account is domain qualified - credentials required to resolve it.
1441         elseif ($credential -ne $null  -or $principalContext -ne $null)
1442         {
1443             Write-Verbose -Message ($LocalizedData.ResolvingDomainAccount -f  $scope, $accountName)
1444         }
1445         else
1446         {
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)
1450         }
1451
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)
1454
1455         $principal = ResolveSidToPrincipal -principalContext $principalContext -sid $sid -isLocalMachineQualified $isLocalMachine
1456         $null = $principals.Add($principal)
1457         $null = $disposables.Add($principal)
1458     }
1459
1460     return $principals.ToArray()
1461 }
1462
1463 <#
1464 .Synopsis
1465     Resolves an array of object names to Principal instances.
1466 #>
1467 function ResolveNamesToPrincipals
1468 {
1469     param
1470     (
1471         [Parameter(Mandatory = $true)]
1472         [ValidateNotNull()]
1473         [String[]] $objectNames,
1474
1475         [Parameter(Mandatory = $true)]
1476         [ValidateNotNull()]
1477         [System.Collections.ArrayList] $disposables,
1478
1479         [Parameter(Mandatory = $true)]
1480         [ValidateNotNull()]
1481         $principalContexts,
1482
1483         [System.Net.NetworkCredential]
1484         $credential = $null
1485     )
1486     Set-StrictMode -Version Latest
1487
1488     [System.Collections.ArrayList] $principals = New-Object System.Collections.ArrayList
1489     $keys = @{}
1490
1491     foreach($objectName in $objectNames)
1492     {
1493         $principal = ResolveNameToPrincipal -principalContexts $principalContexts -credential $credential -disposables $disposables -objectName $objectName
1494         if ($principal -ne $null)
1495         {
1496             [string] $key = $null
1497             # handle duplicate entries
1498             if ($principal.ContextType -eq [System.DirectoryServices.AccountManagement.ContextType]::Domain)
1499             {
1500                 $key = $principal.DistinguishedName
1501             }
1502             else
1503             {
1504                 $key = $principal.SamAccountName
1505             }
1506             if ($keys.ContainsKey($key) -eq $false)
1507             {
1508                 $keys.Add($key, $null)
1509                 $null = $principals.Add($principal)
1510             }
1511         }
1512     }
1513
1514     $keys.Clear()
1515     return $principals.ToArray()
1516 }
1517
1518 <#
1519 .Synopsis
1520     resolves an object name to a Principal
1521 #>
1522 function ResolveNameToPrincipal
1523 {
1524     [OutputType([System.DirectoryServices.AccountManagement.Principal])]
1525     param
1526     (
1527         [Parameter(Mandatory = $true)]
1528         [ValidateNotNull()]
1529         $principalContexts,
1530
1531         [Parameter(Mandatory = $true)]
1532         [ValidateNotNull()]
1533         $disposables,
1534
1535         [Parameter(Mandatory = $true)]
1536         [ValidateNotNullOrEmpty()]
1537         [string] $objectName,
1538
1539         [System.Net.NetworkCredential]
1540         $credential = $null
1541
1542     )
1543     Set-StrictMode -Version Latest
1544
1545     [string] $accountName = $null
1546
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)
1549
1550     # check for an object qualified to the local machine
1551     [bool] $isLocalMachine = IsLocalMachine $scope
1552
1553     [System.DirectoryServices.AccountManagement.PrincipalContext] $principalContext = $null
1554     [bool] $UseDomainTrust = $false
1555
1556     # if local machine qualified, get the PrincipalContext for the local machine
1557     if ($isLocalMachine -eq $true)
1558     {
1559         Write-Verbose -Message ($LocalizedData.ResolvingLocalAccount -f $objectName)
1560     }
1561     # the account is domain qualified - credentials provided to resolve it.
1562     elseif ($credential -ne $null)
1563     {
1564         Write-Verbose -Message ($LocalizedData.ResolvingDomainAccount -f  $ObjectName, $scope)
1565     }
1566     # no credentials provided to resolve account name, so try with domain trust
1567     else
1568     {
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)
1573     }
1574
1575     # Get a PrincipalContext to use to resolve the object
1576     $principalContext = GetPrincipalContext -principalContexts $principalContexts -disposables $disposables -scope $scope -credential $credential
1577     
1578     if ($UseDomainTrust)
1579     {
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
1583     }
1584     else
1585     {
1586         $account = $accountName
1587     }
1588
1589     try
1590     {
1591         [System.DirectoryServices.AccountManagement.Principal] $principal = [System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($principalContext, $account)
1592     }
1593     catch [System.Runtime.InteropServices.COMException]
1594     {
1595         ThrowInvalidArgumentError -ErrorId "PrincipalNotFound" -ErrorMessage ( $LocalizedData.UnableToResolveAccount -f $objectName, $_.Exception.Message, $_.Exception.HResult )
1596     }
1597
1598     if ($principal -eq $null)
1599     {
1600         [string] $errorId = $null
1601         if ($isLocalMachine)
1602         {
1603             $errorId = "PrincipalNotFound_LocalMachine"
1604         }
1605         else
1606         {
1607             $errorId = "PrincipalNotFound_ProvidedCredential"
1608         }
1609
1610         ThrowInvalidArgumentError -ErrorId $errorId -ErrorMessage ($LocalizedData.CouldNotFindPrincipal -f $objectName)
1611     }
1612
1613     return $principal
1614 }
1615
1616 <#
1617 .Synopsis
1618     Resolves a SID to a principal
1619 #>
1620 function ResolveSidToPrincipal
1621 {
1622     [OutputType([System.DirectoryServices.AccountManagement.Principal])]
1623     param
1624     (
1625         [Parameter(Mandatory = $true)]
1626         [ValidateNotNull()]
1627         [System.DirectoryServices.AccountManagement.PrincipalContext] $principalContext,
1628
1629         [Parameter(Mandatory = $true)]
1630         [ValidateNotNull()]
1631         [System.Security.Principal.SecurityIdentifier] $sid,
1632
1633         [Parameter(Mandatory = $true)]
1634         [bool] $isLocalMachineQualified
1635     )
1636     Set-StrictMode -Version Latest
1637
1638     [string] $sidValue = $Sid.Value
1639
1640     # Try to find a matching principal.
1641     $principal = [System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($principalContext, [System.DirectoryServices.AccountManagement.IdentityType]::Sid, $sidValue)
1642
1643     if ($principal -eq $null)
1644     {
1645         [string] $errorId = $null
1646         if ($isLocalMachineQualified)
1647         {
1648             $errorId = "PrincipalNotFound_LocalMachine"
1649         }
1650         else
1651         {
1652             $errorId = "PrincipalNotFound_ProvidedCredential"
1653         }
1654
1655         ThrowInvalidArgumentError -ErrorId $errorId -ErrorMessage ($LocalizedData.CouldNotFindPrincipal -f $sid.ToString())
1656     }
1657
1658     return $principal
1659 }
1660
1661 <#
1662 .Synopsis
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
1670 #>
1671 function GetPrincipalContext
1672 {
1673     [OutputType([System.DirectoryServices.AccountManagement.PrincipalContext])]
1674     param
1675     (
1676         [Parameter(Mandatory = $true)]
1677         [ValidateNotNull()]
1678         $principalContexts,
1679
1680         [Parameter(Mandatory = $true)]
1681         [ValidateNotNull()]
1682         $disposables,
1683
1684         [Parameter(Mandatory = $true)]
1685         [ValidateNotNullOrEmpty()]
1686         [object] $scope,
1687
1688         [System.Net.NetworkCredential]
1689         $credential = $null
1690     )
1691
1692     # The PrincipalContext to use to resolve the account
1693     [System.DirectoryServices.AccountManagement.PrincipalContext] $principalContext = $null
1694
1695     # check for an object qualified to the local machine
1696     [bool] $isLocalMachine = [System.String]::Compare($env:COMPUTERNAME, $scope) -eq 0
1697
1698     if ($isLocalMachine)
1699     {
1700         # check for a cached PrincipalContext for the local machine.
1701         if ($principalContexts.ContainsKey($env:COMPUTERNAME))
1702         {
1703             $principalContext = $principalContexts[$env:COMPUTERNAME]
1704         }
1705         else
1706         {
1707             # Create a PrincipalContext for the local machine
1708             $principalContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Machine)
1709
1710             # Cache the PrincipalContext for this scope for subsequent calls.
1711             $principalContexts.Add($env:COMPUTERNAME, $principalContext)
1712             $null = $disposables.Add($principalContext)
1713         }
1714     }
1715     elseif ($principalContexts.ContainsKey($scope))
1716     {
1717         $principalContext = $principalContexts[$scope]
1718     }
1719     elseif ($credential -ne $null)
1720     {
1721         # Create a PrincipalContext targeing $scope using the network credentials that were passed in.
1722
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)
1725
1726         # Cache the PrincipalContext for this scope for subsequent calls.
1727         $principalContexts.Add($scope, $principalContext)
1728         $null = $disposables.Add($principalContext)
1729     }
1730     else
1731     {
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)
1734
1735         # Cache the PrincipalContext for this scope for subsequent calls.
1736         $principalContexts.Add($scope, $principalContext)
1737         $null = $disposables.Add($principalContext)
1738     }
1739
1740     return $principalContext
1741 }
1742
1743
1744 <#
1745 .Synopsis
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
1749     in $Group.Members
1750 #>function AddGroupMembers
1751 {
1752     [OutputType([bool])]
1753     param
1754     (
1755         [System.DirectoryServices.AccountManagement.GroupPrincipal] $Group,
1756
1757         [System.DirectoryServices.AccountManagement.Principal[]] $Principals
1758     )
1759     Set-StrictMode -Version Latest
1760     [bool] $updated = $false
1761
1762     if ($Principals -ne $null)
1763     {
1764         # Make changes to the group.
1765         foreach($principal in $Principals)
1766         {
1767             if ($group.Members.Contains($principal))
1768             {
1769                 continue
1770             }
1771             $group.Members.Add($principal)
1772             # indicate a change was made to $Group.Members
1773             $updated = $true
1774         }
1775     }
1776     return $updated
1777 }
1778
1779 <#
1780 .Synopsis
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
1784     in $Group.Members
1785 #>
1786 function RemoveGroupMembers
1787 {
1788     [OutputType([bool])]
1789     param
1790     (
1791         [System.DirectoryServices.AccountManagement.GroupPrincipal] $Group,
1792
1793         [System.DirectoryServices.AccountManagement.Principal[]] $Principals
1794     )
1795     Set-StrictMode -Version Latest
1796     [bool] $updated = $false
1797
1798     if ($Principals -ne $null)
1799     {
1800         # Make changes to the group.
1801         foreach($principal in $Principals)
1802         {
1803             if ($group.Members.Remove($principal) -eq $true)
1804             {
1805                 # indicated a change was made to the members.
1806                 $updated = $true
1807             }
1808         }
1809     }
1810
1811     return $updated
1812 }
1813
1814 #region Utilities
1815
1816 <#
1817 .Synopsis
1818     Determines if a scope represents the current machine.
1819 #>
1820 function IsLocalMachine
1821 {
1822     [OutputType([bool])]
1823     param
1824     (
1825         [Parameter(Mandatory=$true)]
1826         [ValidateNotNullOrEmpty()]
1827         [string]
1828         $scope
1829     )
1830     Set-StrictMode -Version latest
1831
1832     if ($scope -eq ".")
1833     {
1834         return $true
1835     }
1836
1837     if ($scope -eq $env:COMPUTERNAME)
1838     {
1839         return $true
1840     }
1841
1842     if ($scope -eq "localhost")
1843     {
1844         return $true
1845     }
1846
1847     if ($scope.Contains("."))
1848     {
1849         if ($scope -eq "127.0.0.1")
1850         {
1851             return $true
1852         }
1853
1854         # Determine if we have an ip address that matches an ip address on one of the
1855         # network adapters.
1856         # NOTE: This is likely overkill; consider removing it.
1857         $items = @(Get-WmiObject Win32_NetworkAdapterConfiguration)
1858         foreach ($item in $items)
1859         {
1860             if ($item.IPaddress -ne $null)
1861             {
1862                 foreach ($addr in $item.IPaddress)
1863                 {
1864                     if ($addr -eq $scope)
1865                     {
1866                         return $true
1867                     }
1868                 }
1869             }
1870         }
1871     }
1872     return $false
1873 }
1874
1875 <#
1876 .Synopsis
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.
1879
1880 .Notes
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.
1884 #>
1885 function IsSameDomain
1886 {
1887     [OutputType([bool])]
1888     param
1889     (
1890         [Parameter(Mandatory=$true)]
1891         [ValidateNotNull()]
1892         [System.DirectoryServices.AccountManagement.PrincipalContext] $principalContext,
1893
1894         [Parameter(Mandatory=$true)]
1895         [ValidateNotNull()]
1896         [String] $domain
1897     )
1898     Set-StrictMode -Version latest
1899
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
1902
1903     if ($isSameDomain -eq $false)
1904     {
1905         $dotIndex = $principalContext.ConnectedServer.IndexOf(".")
1906         if ($dotIndex -ne -1)
1907         {
1908             $principalDomain = $principalContext.ConnectedServer.Substring($dotIndex + 1)
1909             $isSameDomain = [System.String]::Compare($domain, $principalDomain, [System.StringComparison]::OrdinalIgnoreCase) -eq 0
1910         }
1911     }
1912
1913     return $isSameDomain
1914 }
1915
1916 <#
1917 .Synopsis
1918     Parses various object name formats to extract the machine or domain scope.
1919
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.
1923
1924     The following details the formats that are handled as well as how the
1925     values are determined.
1926
1927     Domain qualified names (domainname\username)
1928
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.
1931
1932     UPN: (username@domainname)
1933
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
1936     as the scope
1937
1938     DistinguishedName:
1939
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
1942     name.
1943
1944     Unqualified account names:
1945
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
1949     account names.
1950
1951 .Notes
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
1957     the problem.
1958 #>
1959 function Parse-Scope
1960 {
1961     [OutputType([string])]
1962     param
1963     (
1964         [Parameter(Mandatory=$true)]
1965         [ValidateNotNullOrEmpty()]
1966         [string]
1967         $fullName,
1968
1969         [Parameter(Mandatory=$true)]
1970         [AllowEmptyString()]
1971         [AllowNull()]
1972         [ref] $accountName
1973     )
1974     Set-StrictMode -Version latest
1975
1976     # assume no scope is defined or $fullName is a DistinguishedName
1977     $accountName.Value = $fullName
1978
1979     # parse domain or machine qualified account name
1980     [int] $separatorIndex = $fullName.IndexOf("\")
1981     if ($separatorIndex -ne -1)
1982     {
1983         $scope = $fullName.Substring(0, $separatorIndex)
1984         if (IsLocalMachine $scope)
1985         {
1986             $scope = $env:COMPUTERNAME
1987         }
1988         $accountName.Value = $fullName.Substring($separatorIndex+1)
1989         return $scope
1990     }
1991
1992     # parse UPN for the scope
1993     $separatorIndex = $fullName.IndexOf("@")
1994     if ($separatorIndex -ne -1)
1995     {
1996         $scope = $fullName.Substring($separatorIndex + 1)
1997         $accountName.Value = $fullName.Substring(0,$separatorIndex)
1998         return $scope
1999     }
2000
2001     # parse distinguished name for the scope
2002     $separatorIndex = $fullName.IndexOf("DC=", [System.StringComparison]::OrdinalIgnoreCase)
2003     if ($separatorIndex -ne -1)
2004     {
2005         # NOTE: For distinguished name formats, the DistinguishedName is
2006         # returned as the account name. See the initialization of $accountName
2007         # above.
2008         $startIndex = $separatorIndex + 3
2009         $endIndex = $fullName.IndexOf(",", $startIndex)
2010         if ($endIndex -gt $startIndex)
2011         {
2012             $length = $endIndex - $separatorIndex - 3
2013             $scope = $fullName.Substring($startIndex, $length)
2014             return $scope
2015         }
2016     }
2017     return $env:COMPUTERNAME
2018 }
2019
2020 <#
2021 .Synopsis
2022     Disposes the contents of an array list containing IDisposable objects.
2023 #>
2024 function DisposeAll
2025 {
2026     param
2027     (
2028         [Parameter(Mandatory = $true)]
2029         [ValidateNotNull()]
2030         [AllowEmptyCollection()]
2031         [System.Collections.ArrayList]
2032         $disposables
2033     )
2034     Set-StrictMode -Version latest
2035
2036     if ($disposables.Count -gt 0)
2037     {
2038         foreach ($disposable in $disposables)
2039         {
2040             if ($disposable -is [System.IDisposable])
2041             {
2042                 $disposable.Dispose()
2043             }
2044         }
2045     }
2046 }
2047
2048 <#
2049 .Synopsis
2050     Gets a local windows group
2051
2052 .Notes
2053     The returned value is NOT added to the $disposables list.
2054 #>
2055 function GetGroup
2056 {
2057     [OutputType([System.DirectoryServices.AccountManagement.GroupPrincipal])]
2058     param
2059     (
2060         [Parameter(Mandatory = $true)]
2061         [ValidateNotNullOrEmpty()]
2062         [string] $groupName,
2063
2064         [Parameter(Mandatory = $true)]
2065         [ValidateNotNull()]
2066         [System.Collections.ArrayList]
2067         [AllowEmptyCollection()]
2068         $disposables,
2069
2070         [Parameter(Mandatory = $true)]
2071         [AllowEmptyCollection()]
2072         $principalContexts
2073     )
2074
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.
2079     return $group
2080 }
2081
2082 <#
2083 .Synopsis
2084 Validates the Group name for invalid characters.
2085 #>
2086 function ValidateGroupName
2087 {
2088     param
2089     (
2090         [parameter(Mandatory = $true)]
2091         [ValidateNotNullOrEmpty()]
2092         [System.String]
2093         $GroupName
2094     )
2095
2096     # Check if the name consists of only periods and/or white spaces.
2097     [bool] $wrongName = $true
2098
2099     for($i = 0; $i -lt $GroupName.Length; $i++)
2100     {
2101         if(-not [Char]::IsWhiteSpace($GroupName, $i) -and $GroupName[$i] -ne '.')
2102         {
2103             $wrongName = $false
2104             break
2105         }
2106     }
2107
2108     $invalidChars = @('\','/','"','[',']',':','|','<','>','+','=',';',',','?','*','@')
2109
2110     if($wrongName)
2111     {
2112         ThrowInvalidArgumentError -ErrorId "GroupNameHasOnlyWhiteSpacesAndDots" -ErrorMessage ($LocalizedData.InvalidGroupName -f $GroupName, [string]::Join(" ", $invalidChars))
2113     }
2114
2115     if($GroupName.IndexOfAny($invalidChars) -ne -1)
2116     {
2117         ThrowInvalidArgumentError -ErrorId "GroupNameHasInvalidCharachter" -ErrorMessage ($LocalizedData.InvalidGroupName -f $GroupName, [string]::Join(" ", $invalidChars))
2118     }
2119 }
2120
2121 <#
2122 .Synopsis
2123 Throws an argument error.
2124 #>
2125 function ThrowInvalidArgumentError
2126 {
2127     [CmdletBinding()]
2128     param
2129     (
2130
2131         [parameter(Mandatory = $true)]
2132         [ValidateNotNullOrEmpty()]
2133         [System.String]
2134         $ErrorId,
2135
2136         [parameter(Mandatory = $true)]
2137         [ValidateNotNullOrEmpty()]
2138         [System.String]
2139         $ErrorMessage
2140     )
2141
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
2145     throw $errorRecord
2146 }
2147
2148 Function Throw-TerminatingError
2149 {
2150     param(
2151         [string] $Message,
2152         [System.Management.Automation.ErrorRecord] $ErrorRecord,
2153         [string] $ExceptionType
2154     )
2155     
2156     $exception = new-object "System.InvalidOperationException" $Message,$ErrorRecord.Exception
2157     $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"MachineStateIncorrect","InvalidOperation",$null
2158     throw $errorRecord
2159 }
2160
2161 <#
2162 .Synopsis
2163 Writes either to Verbose or ShouldProcess channel.
2164 #>
2165 function Write-Log
2166 {
2167     [CmdletBinding(SupportsShouldProcess=$true)]
2168     param
2169     (
2170         [parameter(Mandatory = $true)]
2171         [ValidateNotNullOrEmpty()]
2172         [System.String]
2173         $Message
2174     )
2175
2176     if ($PSCmdlet.ShouldProcess($Message, $null, $null))
2177     {
2178         Write-Verbose $Message
2179     }
2180 }
2181
2182 #endregion
2183
2184 Export-ModuleMember -function Get-TargetResource, Set-TargetResource, Test-TargetResource