PSDscResources icon indicating copy to clipboard operation
PSDscResources copied to clipboard

Group: Resource failing when using 'MembersToInclude' to add members to a local group that contains a domain group

Open aydeisen opened this issue 7 years ago • 40 comments

I have a DSC file with a resource hashtable configured similar to the following:

Group AdminGroupMembership {
    Ensure           = "Present"
    GroupName        = "Administrators"
    MembersToInclude = @("Member01")
}

When the DSC applies to a domain member server, it fails, even though 'Member01' was successfully added to the group.

What appears to be happening is that MembersToInclude has the resource iterating through all the members of the group to see if 'Member01' is present. Since this is a domain member server, and since PsDscRunAsCredential isn't specified, the failure is being generated when the script attempts to process the [DOMAIN]\Domain Admins group, as it doesn't have access.

The outcome I was is expecting is: since I'm only looking to validate that 'Member01' exists inside the Administrators local group, and since I'm not looking to remove any members from it, DSC would only validate that 'Member01' exists inside the Administrators local group, and it wouldn't try to resolve a domain member when it doesn't have access to Active Directory.

Is this "working as intended" or can this be resolved in a future update?

aydeisen avatar Feb 08 '18 17:02 aydeisen

I'm able to check domain members/domain groups by specifying the runas credential.
to create the credential.dat file use:

$credentials = get-credential $credentials | export-clixml powershelluser.dat

then your DSC config will look like:

$CredentialFolder = "$PSScriptRoot\credentials" $powershelljobs_svc = "$CredentialFolder\powershelluser.dat"

Configuration MyConfig { Group StandardLocalAdmins { GroupName = 'Administrators' Ensure = 'Present' MembersToInclude = @( 'LocalAdminUser', 'DOMAIN\Domain Admins' ) PsDscRunAsCredential = Import-Clixml $powershelluser } }

ewhitesides avatar Mar 12 '18 20:03 ewhitesides

@ewhitesides: but that's the point. I have no desire to include 'DOMAIN\Domain Admins' in my DSC Configuration and, by using MembersToInclude, I'm not making my configuration overwrite the unlisted accounts in the group. So, why should I need to include PsDscRunAsCredential when there's nothing included in my DSC configuration that manipulates domain accounts. By extension, if I'm not including anything that manipulates domain accounts in my DSC configuration, why is it trying to enumerate the members of a domain group when doing so is not defined in the DSC configuration

aydeisen avatar Mar 12 '18 20:03 aydeisen

it most likely has to do with the way the group resource module was coded. it probably detects the computer is a member of a domain, and switches to requiring a domain account for verifying even the local only accounts.

In situations like this, you're best off just using the Script resource to build something on your own.

Test-Script could involve something like '$UserCheck = &net localgroup administrators' and checking if the user is in the $UserCheck variable.

Set-Script would involve a command like 'net localgroup administrators /add'

ewhitesides avatar Mar 12 '18 21:03 ewhitesides

@ewhitesides Why would I want to use a workaround for a resource of this project when it isn't working as documented? This is a bugfix request. If the owners of this project aren't going to implement a fix, they can at least have the courtesy of reviewing the issue and marking it as such.

aydeisen avatar Mar 12 '18 21:03 aydeisen

@aydeisen what error message are you seeing when this happens? I guess you are running the resource as SYSTEM?

I guess the problem is in Get-MembersAsPrincipalsList. I gather by this comment that this should work, but something is going on that prevents it from "Dropping down to the underyling DirectoryEntry API".

https://github.com/PowerShell/PSDscResources/blob/9f5e9e48eda63b5ff0ab5ad179173b754863c49e/DscResources/MSFT_GroupResource/MSFT_GroupResource.psm1#L1666-L1674

johlju avatar Mar 13 '18 18:03 johlju

@aydeisen Also, could you please provide the verbose output when you get this error?

johlju avatar Mar 13 '18 18:03 johlju

@johlju This is the error message:

PowerShell DSC resource MSFT_GroupResource  failed to execute Test-TargetResource functionality with error message: Exception calling ".ctor" with "2" argument(s): "The server could not be contacted." 
+ CategoryInfo          : InvalidOperation: (:) [], CimException
+ FullyQualifiedErrorId : ProviderOperationExecutionFailure
+ PSComputerName        : localhost

...and this is the verbose log for the resource (with username, hostname, and domain name replacement for company compliance reasons):

[SERVER06]: LCM:  [ Start  Resource ]  [[Group]AdminGroupMembership]
[SERVER06]: LCM:  [ Start  Test     ]  [[Group]AdminGroupMembership]
[SERVER06]:                            [[Group]AdminGroupMembership] Invoking the function Test-TargetResourceOnFullSKU for the group Administrators.
[SERVER06]:                            [[Group]AdminGroupMembership] A group with the name Administrators exists.
[SERVER06]:                            [[Group]AdminGroupMembership] Resolving Administrator as a local account.
[SERVER06]:                            [[Group]AdminGroupMembership] Resolving IrrelevantLocalUser01 as a local account.
[SERVER06]:                            [[Group]AdminGroupMembership] Resolving IrrelevantLocalUser02 as a local account.
[SERVER06]:                            [[Group]AdminGroupMembership] Resolving Member01 as a local account.
[SERVER06]:                            [[Group]AdminGroupMembership] Resolving IrrelevantLocalUser03 as a local account.
[SERVER06]:                            [[Group]AdminGroupMembership] Resolving Domain Admins in the domain CONTOSO.
[SERVER06]: LCM:  [ End    Test     ]  [[Group]AdminGroupMembership]  in 46.4910 seconds.
PowerShell DSC resource MSFT_GroupResource  failed to execute Test-TargetResource functionality with error message: Exception calling ".ctor" with "2" argument(s): "The server could not be contacted." 
+ CategoryInfo          : InvalidOperation: (:) [], CimException
+ FullyQualifiedErrorId : ProviderOperationExecutionFailure
+ PSComputerName        : localhost

aydeisen avatar Mar 13 '18 20:03 aydeisen

@aydeisen okay, so it seems that Test-TargetResource functin fails, because it seems it never start the Set-TargetResource, it ends with [ End Test ] and then the error is reported.

Questions when looking at trying to reproduce this behavior.

  1. What version of PSDscResources are you using?
  2. 'Member01' in your configuration above, is it a local account or a domain account?
  3. Is the server able to connect to the domain controller? Thought if this was member server but then is disconnected by a firewall for instance. I know the point is shouldn't need to connect, just looking for the scenario where this fails.

johlju avatar Mar 17 '18 13:03 johlju

Tried to reproduced using

  • Windows Server 2012 R2
  • WMF 5.1
  • PSDscResource v2.8.0.0
  • Target node is a member server
  • Target node is able to access a Domain Controller.

With those parameters I cannot reproduce the behavior.

Configuration

$newObjectParameters = @{
    TypeName = 'System.Management.Automation.PSCredential'
    ArgumentList = @(
        'Dummy',
        (ConvertTo-SecureString 'Password/2/(&(/' -AsPlainText -Force)
    )
}
$localDummyCredential = New-Object @newObjectParameters

User 'CreateLocalAccountDummy'
{
    Ensure   = 'Present'
    UserName = $localDummyCredential.UserName
    Password = $localDummyCredential
}

Group 'AddLocalAccountToLocalAdministratorsGroup'
{
    Ensure           = 'Present'
    GroupName        = 'Administrators'
    MembersToInclude = @($localDummyCredential.UserName)
}

Result:

VERBOSE: [SQLTEST]: LCM:  [ Start  Resource ]  [[User]CreateLocalAccountDummy]
VERBOSE: [SQLTEST]: LCM:  [ Start  Test     ]  [[User]CreateLocalAccountDummy]
VERBOSE: [SQLTEST]:                            [[User]CreateLocalAccountDummy] A user with the name Dummy does not exist.
VERBOSE: [SQLTEST]: LCM:  [ End    Test     ]  [[User]CreateLocalAccountDummy]  in 5.8920 seconds.
VERBOSE: [SQLTEST]: LCM:  [ Start  Set      ]  [[User]CreateLocalAccountDummy]
VERBOSE: [SQLTEST]:                            [[User]CreateLocalAccountDummy] Configuration of user Dummy started.
VERBOSE: [SQLTEST]:                            [[User]CreateLocalAccountDummy] User: Dummy
VERBOSE: [SQLTEST]:                            [[User]CreateLocalAccountDummy] User Dummy created successfully.
VERBOSE: [SQLTEST]:                            [[User]CreateLocalAccountDummy] Configuration of user Dummy completed successfully.
VERBOSE: [SQLTEST]: LCM:  [ End    Set      ]  [[User]CreateLocalAccountDummy]  in 3.2190 seconds.
VERBOSE: [SQLTEST]: LCM:  [ End    Resource ]  [[User]CreateLocalAccountDummy]
VERBOSE: [SQLTEST]: LCM:  [ Start  Resource ]  [[Group]AddLocalAccountToLocalAdministratorsGroup]
VERBOSE: [SQLTEST]: LCM:  [ Start  Test     ]  [[Group]AddLocalAccountToLocalAdministratorsGroup]
VERBOSE: [SQLTEST]:                            [[Group]AddLocalAccountToLocalAdministratorsGroup] Invoking the function Test-TargetResourceOnFullSKU for the group Administrators.
VERBOSE: [SQLTEST]:                            [[Group]AddLocalAccountToLocalAdministratorsGroup] A group with the name Administrators exists.
VERBOSE: [SQLTEST]:                            [[Group]AddLocalAccountToLocalAdministratorsGroup] Resolving Administrator as a local account.
VERBOSE: [SQLTEST]:                            [[Group]AddLocalAccountToLocalAdministratorsGroup] Resolving Domain Admins in the domain COMPANY.
VERBOSE: [SQLTEST]:                            [[Group]AddLocalAccountToLocalAdministratorsGroup] Resolving Dummy as a local account.
VERBOSE: [SQLTEST]:                            [[Group]AddLocalAccountToLocalAdministratorsGroup] MembersToExclude is empty. No group member removals are needed.
VERBOSE: [SQLTEST]: LCM:  [ End    Test     ]  [[Group]AddLocalAccountToLocalAdministratorsGroup]  in 3.5160 seconds.
VERBOSE: [SQLTEST]: LCM:  [ Start  Set      ]  [[Group]AddLocalAccountToLocalAdministratorsGroup]
VERBOSE: [SQLTEST]:                            [[Group]AddLocalAccountToLocalAdministratorsGroup] Begin executing Set functionality on the group Administrators.
VERBOSE: [SQLTEST]:                            [[Group]AddLocalAccountToLocalAdministratorsGroup] Performing the operation "Set" on target "Group: Administrators".
VERBOSE: [SQLTEST]:                            [[Group]AddLocalAccountToLocalAdministratorsGroup] Resolving Administrator as a local account.
VERBOSE: [SQLTEST]:                            [[Group]AddLocalAccountToLocalAdministratorsGroup] Resolving Domain Admins in the domain COMPANY.
VERBOSE: [SQLTEST]:                            [[Group]AddLocalAccountToLocalAdministratorsGroup] Resolving Dummy as a local account.
VERBOSE: [SQLTEST]:                            [[Group]AddLocalAccountToLocalAdministratorsGroup] MembersToExclude is empty. No group member removals are needed.
VERBOSE: [SQLTEST]:                            [[Group]AddLocalAccountToLocalAdministratorsGroup] Group Administrators properties updated successfully.
VERBOSE: [SQLTEST]:                            [[Group]AddLocalAccountToLocalAdministratorsGroup] End executing Set functionality on the group Administrators.
VERBOSE: [SQLTEST]: LCM:  [ End    Set      ]  [[Group]AddLocalAccountToLocalAdministratorsGroup]  in 2.9580 seconds.
VERBOSE: [SQLTEST]: LCM:  [ End    Resource ]  [[Group]AddLocalAccountToLocalAdministratorsGroup]

johlju avatar Mar 17 '18 13:03 johlju

@johlju

What version of PSDscResources are you using?

2.8.0.0

'Member01' in your configuration above, is it a local account or a domain account?

Local Account

Is the server able to connect to the domain controller? Thought if this was member server but then is disconnected by a firewall for instance. I know the point is shouldn't need to connect, just looking for the scenario where this fails.

The server itself is a domain member server, and is able to communicate with a domain controller.

aydeisen avatar Mar 19 '18 10:03 aydeisen

@aydeisen Is there any difference in your environment from what I used trying to reproduce this? See bullet list in my previous comment. Do you see anything else in your environment that could possibly make this behavior happen? Could you try the above code in a lab/test environment and see if that gives the same behavior?

I see the following verbose output of the Test-TargetResource function in my verbose output, but not yours.

MembersToExclude is empty. No group member removals are needed.

Would it be possible for you to add more verbose output in the code to see which row it is actually failing on?

johlju avatar Mar 19 '18 15:03 johlju

@johlju There's nothing I can think of in the environment that would cause any difference. I have nine different servers with pretty vanilla builds running a mix of 2008 R2 SP1 and 2012 R2, and they're all displaying this error.

I'm not sure how I can get any more verbose without wrapping Trace-Command around it. That output came directly from executing Start-DscConfiguration -Wait -Verbose. What I put in my previous update is the entirety of the output for that DSC Resource. I assume my output doesn't have it because your output proceeds past the Domain Admins member, and mine fails at that point.

aydeisen avatar Mar 19 '18 17:03 aydeisen

Since this is only reproducible in your environment, you need to debug this to find the actual row that is failing. Maybe then it's possible to determine why it is happening.

On line 1241 the MembersToExclude is empty message is written out, so something happens before then.

You can debug resource by using Enable-DscDebug, see Debugging DSC resources.

Another way to debug this is to copy the psm1 file to a ps1 file, open the ps1 file in ISE for example and manually call `Test-TargetResource' with the correct paramaters and debug each step.

Or before line 1241 and after the else-if statment, add Write-Verbose -Message 'Debug1', and so on, after each row that is called. If there is a helper function that throws, then continue the process in that helper function, and so on.

johlju avatar Mar 19 '18 17:03 johlju

@johlju An error is being thrown from the Remove-DisposableObject function in the Group resource. Error is Cannot convert value to type System.String. The value it's attempting to convert appears to be an empty string. The HResult is -2146233087 (0x80131501) This is the full debug for the Domain Admins group:

DEBUG: 1677+     foreach ( >>>$groupDirectoryMember in $groupDirectoryMembers)
DEBUG:     ! SET $groupDirectoryMember = 'System.__ComObject'.
DEBUG: 1680+          >>>$memberDirectoryEntry = New-Object -TypeName 'System.DirectoryServices.DirectoryEntry' `
DEBUG:     ! SET $memberDirectoryEntry = 'System.DirectoryServices.DirectoryEntry'.
DEBUG: 1682+          >>>$null = $disposables.Add($memberDirectoryEntry)
DEBUG: 1684+          >>>$memberDirectoryEntryPathParts = $memberDirectoryEntry.Path.Split('/')
DEBUG:     ! SET $memberDirectoryEntryPathParts = 'WinNT:  CONTOSO Domain Admins'.
DEBUG: 1686+         if ( >>>$memberDirectoryEntryPathParts.Count -eq 4)
DEBUG: 1689+              >>>$scope = $memberDirectoryEntryPathParts[2]
DEBUG:     ! SET $scope = 'CONTOSO'.
DEBUG: 1690+              >>>$accountName = $memberDirectoryEntryPathParts[3]
DEBUG:     ! SET $accountName = 'Domain Admins'.
DEBUG: 1711+          >>>$principalContext = Get-PrincipalContext `
DEBUG: 2054+  >>>{
DEBUG:     ! CALL function 'Get-PrincipalContext'  (defined in file 'C:\Program Files\WindowsPowerShell\Modules\PSDscResources\2.8.0.0\DscResources\MSFT_GroupResource\MSFT_GroupResource.ps1')
DEBUG: 2081+      >>>$principalContext = $null
DEBUG:     ! SET $principalContext = ''.
DEBUG: 2083+     if ( >>>Test-IsLocalMachine -Scope $Scope)
DEBUG: 2083+     if ( >>>Test-IsLocalMachine -Scope $Scope)
DEBUG: 2149+  >>>{
DEBUG:     ! CALL function 'Test-IsLocalMachine'  (defined in file 'C:\Program Files\WindowsPowerShell\Modules\PSDscResources\2.8.0.0\DscResources\MSFT_GroupResource\MSFT_GroupResource.ps1')
DEBUG: 2160+      >>>$localMachineScopes = @( '.', $env:computerName, 'localhost', '127.0.0.1', 'NT Authority', 'NT Service', 'BuiltIn' )
DEBUG:     ! SET $localMachineScopes = '. SERVER06 localhost 127.0.0.1 NT Autho...'.
DEBUG: 2162+     if ( >>>$localMachineScopes -icontains $Scope)
DEBUG: 2171+     if ( >>>$Scope.Contains('.'))
DEBUG: 2189+     return  >>>$false
DEBUG: 2190+  >>>}
DEBUG: 2101+     elseif ( >>>$PrincipalContextCache.ContainsKey($Scope))
DEBUG: 2105+     elseif ( >>>$null -ne $Credential)
DEBUG: 2130+          >>>$principalContext = New-Object -TypeName 'System.DirectoryServices.AccountManagement.PrincipalContext' `
DEBUG:     ! SET $principalContext = 'System.DirectoryServices.AccountManagement.P...'.
DEBUG: 2134+          >>>$null = $PrincipalContextCache.Add($Scope, $principalContext)
DEBUG: 2135+          >>>$null = $Disposables.Add($principalContext)
DEBUG: 2138+     return  >>>$principalContext
DEBUG: 2139+  >>>}
DEBUG:     ! SET $principalContext = 'System.DirectoryServices.AccountManagement.P...'.
DEBUG: 1718+         if ( >>>Test-IsLocalMachine -Scope $scope)
DEBUG: 1718+         if ( >>>Test-IsLocalMachine -Scope $scope)
DEBUG: 2149+  >>>{
DEBUG:     ! CALL function 'Test-IsLocalMachine'  (defined in file 'C:\Program Files\WindowsPowerShell\Modules\PSDscResources\2.8.0.0\DscResources\MSFT_GroupResource\MSFT_GroupResource.ps1')
DEBUG: 2160+      >>>$localMachineScopes = @( '.', $env:computerName, 'localhost', '127.0.0.1', 'NT Authority', 'NT Service', 'BuiltIn' )
DEBUG:     ! SET $localMachineScopes = '. SERVER06 localhost 127.0.0.1 NT Autho...'.
DEBUG: 2162+     if ( >>>$localMachineScopes -icontains $Scope)
DEBUG: 2171+     if ( >>>$Scope.Contains('.'))
DEBUG: 2189+     return  >>>$false
DEBUG: 2190+  >>>}
DEBUG: 1723+         elseif ( >>>$null -ne $principalContext)
DEBUG: 1725+              >>>Write-Verbose -Message ($script:localizedData.ResolvingDomainAccount -f $accountName, $scope)
DEBUG: 1739+          >>>$memberSidBytes = $memberDirectoryEntry.Properties['ObjectSid'].Value
DEBUG:     ! SET $memberSidBytes = '1 5 0 0 0 0 0 5 21 0 0 0 221 41 76 245 89 223 ...'.
DEBUG: 1740+          >>>$memberSid = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' `
DEBUG:     ! SET $memberSid = 'S-1-5-21-4115409373-4274446169-823295157-512'.
DEBUG: 1743+          >>>$principal = Resolve-SidToPrincipal -PrincipalContext $principalContext -Sid $memberSid -Scope $scope
DEBUG: 1995+  >>>{
DEBUG:     ! CALL function 'Resolve-SidToPrincipal'  (defined in file 'C:\Program Files\WindowsPowerShell\Modules\PSDscResources\2.8.0.0\DscResources\MSFT_GroupResource\MSFT_GroupResource.ps1')
DEBUG: 2015+      >>>$principal = Find-Principal -PrincipalContext $PrincipalContext -IdentityValue $Sid.Value -IdentityType ([System.DirectoryServices.AccountManagement.IdentityType]::Sid)
DEBUG: 2319+  >>>{
DEBUG:     ! CALL function 'Find-Principal'  (defined in file 'C:\Program Files\WindowsPowerShell\Modules\PSDscResources\2.8.0.0\DscResources\MSFT_GroupResource\MSFT_GroupResource.ps1')
DEBUG: 2336+     if ( >>>$PSBoundParameters.ContainsKey('IdentityType'))
DEBUG: 2338+         return  >>>[System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($PrincipalContext, $IdentityType, $IdentityValue)
DEBUG:     ! SET $foreach = ''.
DEBUG: 1284+          >>>Remove-DisposableObject -Disposables $disposables
DEBUG: 2554+  >>>{
DEBUG:     ! CALL function 'Remove-DisposableObject'  (defined in file 'C:\Program Files\WindowsPowerShell\Modules\PSDscResources\2.8.0.0\DscResources\MSFT_GroupResource\MSFT_GroupResource.ps1')
DEBUG: 2565+     foreach ($disposable in  >>>$Disposables)
DEBUG:     ! SET $foreach = 'IEnumerator'.
DEBUG: 2565+     foreach ( >>>$disposable in $Disposables)
DEBUG:     ! SET $disposable = 'System.DirectoryServices.AccountManagement.Princip...'.
DEBUG: 2567+         if ( >>>$disposable -is [System.IDisposable])
DEBUG: 2569+              >>>$disposable.Dispose()
DEBUG: 2565+     foreach ( >>>$disposable in $Disposables)
DEBUG:     ! SET $foreach = ''.
Remove-DisposableObject : Cannot convert value to type System.String.
At C:\Program Files\WindowsPowerShell\Modules\PSDscResources\2.8.0.0\DscResources\MSFT_GroupResource\MSFT_GroupResource.ps1:1284 char:9
\+         Remove-DisposableObject -Disposables $disposables
\+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    \+ CategoryInfo          : InvalidArgument: (:) [Remove-DisposableObject], RuntimeException
    \+ FullyQualifiedErrorId : InvalidCastFromAnyTypeToString,Remove-DisposableObject

This is the Stack Trace from the error:

   at System.Management.Automation.ExceptionHandlingOps.CheckActionPreference(FunctionContext funcContext, Exception exception)
   at System.Management.Automation.Interpreter.ActionCallInstruction`2.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.Interpreter.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.LightLambda.RunVoid1[T0](T0 arg0)
   at System.Management.Automation.DlrScriptCommandProcessor.RunClause(Action`1 clause, Object dollarUnderbar, Object inputToProcess)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Management.Automation.Internal.PipelineProcessor.SynchronousExecuteEnumerate(Object input)
   at System.Management.Automation.Runspaces.LocalPipeline.InvokeHelper()
   at System.Management.Automation.Runspaces.LocalPipeline.InvokeThreadProc()

This is the Script Stack Trace from the error:

at Remove-DisposableObject, C:\Program Files\WindowsPowerShell\Modules\PSDscResources\2.8.0.0\DscResources\MSFT_GroupResource\MSFT_GroupResource.ps1: line 2565
at Test-TargetResourceOnFullSKU, C:\Program Files\WindowsPowerShell\Modules\PSDscResources\2.8.0.0\DscResources\MSFT_GroupResource\MSFT_GroupResource.ps1: line 1284
at Test-TargetResource, C:\Program Files\WindowsPowerShell\Modules\PSDscResources\2.8.0.0\DscResources\MSFT_GroupResource\MSFT_GroupResource.ps1: line 321
at <ScriptBlock>, <No file>: line 7

aydeisen avatar Mar 19 '18 19:03 aydeisen

Sorry that I haven't had time to answer you for so long 😞 Thanks for the detailed debugging steps!

DEBUG: ! CALL function 'Remove-DisposableObject' (defined in file 'C:\Program Files\WindowsPowerShell\Modules\PSDscResources\2.8.0.0\DscResources\MSFT_GroupResource\MSFT_GroupResource.ps1')
DEBUG: 2565+ foreach ($disposable in >>>> $Disposables)

DEBUG: ! SET $foreach = 'IEnumerator'.
DEBUG: 2565+ foreach ( >>>> $disposable in $Disposables)

DEBUG: ! SET $disposable = 'System.DirectoryServices.AccountManagement.Princip...'.
DEBUG: 2567+ if ( >>>> $disposable -is [System.IDisposable])

DEBUG: 2569+ >>>> $disposable.Dispose()

DEBUG: 2565+ foreach ( >>>> $disposable in $Disposables)

DEBUG: ! SET $foreach = ''.
Remove-DisposableObject : Cannot convert value to type System.String.
At C:\Program Files\WindowsPowerShell\Modules\PSDscResources\2.8.0.0\DscResources\MSFT_GroupResource\MSFT_GroupResource.ps1:1284 char:9
+ Remove-DisposableObject -Disposables $disposables

It looks like the first value of $Disposables returned

SET $disposable = 'System.DirectoryServices.AccountManagement.Princip...'

But the second value returned an empty string instead of a $null value. 🤔

SET $foreach = ''.

Debug actions:

  1. Could write verbose output in the Remove-DisposableObject helper function. It would be interesting how many objects $Disposables contains $Disposables.Count , and also for each object, what type it is, i.e. $Disposables[0].GetType() .

  2. It would also be interesting for every time the $disposables is used after the array is initialized to find out where the empty string is added. https://github.com/PowerShell/PSDscResources/blob/9f5e9e48eda63b5ff0ab5ad179173b754863c49e/DscResources/MSFT_GroupResource/MSFT_GroupResource.psm1#L1118

johlju avatar Apr 30 '18 13:04 johlju

@johlju: the server I was using for output isn't finding any domain controllers at the moment, so I need some time to troubleshoot and resolve that. The output (below) was run under the local admin account, and might, at the very least, answer the 'type' question.

It looks like there are four types being stored in $Disposables: From System.DirectoryServices.AccountManagement: PrincipalContext, GroupPrincipal, and UserPrincipal The fourth is 'adsi', which I assume means that both the .NET representation and the underlying ADSI object is being stored in the $Disposables variable.

It also looks like Remove-DisposableObject is processing all objects in one shot at the end.

VERBOSE: [SERVER06]: LCM:  [ Start  Resource ]  [[Group]AdminGroupMembership]
VERBOSE: [SERVER06]: LCM:  [ Start  Test     ]  [[Group]AdminGroupMembership]
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Invoking the function Test-TargetResourceOnFullSKU for the group Administrators.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] A group with the name Administrators exists.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Resolving Administrator as a local account.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Resolving IrrelevantUserAccount01 as a local account.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Resolving IrrelevantUserAccount02 as a local account.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Resolving Member01 as a local account.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Resolving IrrelevantAccount03 as a local account.
WARNING: [SERVER06]:                            [[Group]AdminGroupMembership] The group member WinNT://S-1-5-21-4115409373-4274446169-823295157-512 does not exist or cannot be resolved.
WARNING: [SERVER06]:                            [[Group]AdminGroupMembership] The group member WinNT://S-1-5-21-1004336348-1085031214-725345543-512 does not exist or cannot be resolved.
WARNING: [SERVER06]:                            [[Group]AdminGroupMembership] The group member WinNT://S-1-5-21-1004336348-1085031214-725345543-5129 does not exist or cannot be resolved.
WARNING: [SERVER06]:                            [[Group]AdminGroupMembership] The group member WinNT://S-1-5-21-1004336348-1085031214-725345543-13277 does not exist or cannot be resolved.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Resolving ipautomckessonprod as a local account.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] MembersToExclude is empty. No group member removals are needed.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Disposable Count: 16
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] System.DirectoryServices.AccountManagement.PrincipalContext
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] System.DirectoryServices.AccountManagement.GroupPrincipal
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] adsi
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] System.DirectoryServices.AccountManagement.UserPrincipal
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] adsi
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] System.DirectoryServices.AccountManagement.UserPrincipal
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] adsi
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] System.DirectoryServices.AccountManagement.UserPrincipal
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] adsi
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] System.DirectoryServices.AccountManagement.UserPrincipal
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] adsi
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] System.DirectoryServices.AccountManagement.UserPrincipal
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] adsi
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] adsi
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] adsi
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] adsi
VERBOSE: [SERVER06]: LCM:  [ End    Test     ]  [[Group]AdminGroupMembership]  in 2.4270 seconds.
VERBOSE: [SERVER06]: LCM:  [ Skip   Set      ]  [[Group]AdminGroupMembership]
VERBOSE: [SERVER06]: LCM:  [ End    Resource ]  [[Group]AdminGroupMembership]

aydeisen avatar Apr 30 '18 21:04 aydeisen

@aydeisen No worries, great if you can get it to reproduce the problem again. Would be interesting what the type is on the item that fails, and what the value is that it can't convert to string.

johlju avatar May 01 '18 07:05 johlju

@johlju : thanks for your patience. I have a new output after correcting the server's connectivity to AD.

I forgot to reference the function to show the changes in my last posting. I've also augmented it to show the name of the disposable with the type. This is the Remove-DisposableObject function with the additional Write-Verbose parameters:

<#
    .SYNOPSIS
        Disposes of the contents of an array list containing IDisposable objects.

    .PARAMETER Disosables
        The array list of IDisposable Objects to dispose of.
#>
function Remove-DisposableObject
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.Collections.ArrayList]
        [AllowEmptyCollection()]
        $Disposables
    )
    Write-Verbose -Message "`$Disposables Count: $($Disposables.Count)"
    foreach ($disposable in $Disposables)
    {
        Write-Verbose -Message "Name: $($Disposable.Name)   Type: $($Disposable.GetType())"
        if ($disposable -is [System.IDisposable])
        {
            $disposable.Dispose()
        }
    }
}

...And this is the Verbose output:

VERBOSE: [SERVER06]: LCM:  [ Start  Resource ]  [[Group]AdminGroupMembership]
VERBOSE: [SERVER06]: LCM:  [ Start  Test     ]  [[Group]AdminGroupMembership]
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Invoking the function Test-TargetResourceOnFullSKU for the group Administrators.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] A group with the name Administrators exists.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Resolving Administrator as a local account.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Resolving IrrelevantUserAccount01 as a local account.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Resolving IrrelevantUserAccount02 as a local account.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Resolving Member01 as a local account.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Resolving IrrelevantUserAccount03 as a local account.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Resolving Domain Admins in the domain CONTOSO.
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] $Disposables Count: 16
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Name:    Type: System.DirectoryServices.AccountManagement.PrincipalContext
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Name:    Type: System.DirectoryServices.AccountManagement.GroupPrincipal
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Name: Administrator   Type: adsi
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Name:    Type: System.DirectoryServices.AccountManagement.UserPrincipal
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Name: IrrelevantUserAccount01   Type: adsi
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Name:    Type: System.DirectoryServices.AccountManagement.UserPrincipal
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Name: IrrelevantUserAccount02   Type: adsi
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Name:    Type: System.DirectoryServices.AccountManagement.UserPrincipal
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Name: Member01   Type: adsi
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Name:    Type: System.DirectoryServices.AccountManagement.UserPrincipal
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Name: IrrelevantUserAccount03   Type: adsi
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Name:    Type: System.DirectoryServices.AccountManagement.UserPrincipal
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Name: Domain Admins   Type: adsi
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Name: CONTOSO   Type: System.DirectoryServices.AccountManagement.PrincipalContext
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Name:    Type: System.DirectoryServices.AccountManagement.GroupPrincipal
VERBOSE: [SERVER06]:                            [[Group]AdminGroupMembership] Name:    Type: adsi
VERBOSE: [SERVER06]: LCM:  [ End    Test     ]  [[Group]AdminGroupMembership]  in 64.7820 seconds.

I'm counting nine times when the principal name comes up empty: eight times for an object type in the System.DirectoryServices.AccountManagement namespace, and one time for an object of the adsi type. It appears the only disposable in the System.DirectoryServices.AccountManagement namespace to return in the domain as the principal context.

Probably unrelated to my issue, but I'm a little confused as to why the ADSI object is needed at all. I see it's coming from the Get-GroupMembersFromDirectoryEntry function in the module when the GetUnderlyingObject() method is called on the group in order to retrieve its members. However, the .NET Group Principal object already has a Members property. So why the unnecessary overhead?

That function is below for reference:

<#
    .SYNOPSIS
        Retrieves the members of a group from the underlying directory entry.

    .PARAMETER Group
        The group to retrieve the members of.
#>
function Get-GroupMembersFromDirectoryEntry
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.DirectoryServices.AccountManagement.GroupPrincipal]
        $Group
    )

    $groupDirectoryEntry = $Group.GetUnderlyingObject()
    return $groupDirectoryEntry.Invoke('Members')
}

aydeisen avatar May 08 '18 15:05 aydeisen

@aydeisen I'm not seeing the error being thrown, doesn't it throw anymore?

I can see two potential reasons for using GetUnderlyingObject, first the restrictions mentioned in the Remarks section, and second, but not certain at all about this, might be that it is not cached data?

johlju avatar May 09 '18 08:05 johlju

@aydeisen My bad, it throws directly after that, you just skipped that when pasting in the verbose output.

It seem it throws when there is an 'adsi' object with an empty name? Now it would be interesting to find out where it adds that item, and what that item actually is. Could it be here it adds the value (per the function you referenced).

https://github.com/PowerShell/PSDscResources/blob/9f5e9e48eda63b5ff0ab5ad179173b754863c49e/DscResources/MSFT_GroupResource/MSFT_GroupResource.psm1#L1675-L1682

A thought, grasping as straws, what if there could be an issue that Members doesn't contain all the information? 🤔 Maybe RefreshCache() is needed for the Members property, see StackOverflow article UserPrincipal GetUnderlyingObject: properties missing.

johlju avatar May 09 '18 08:05 johlju

@johjlu: regarding the members property of the. NET object: Wouldn't using the GetMembers() method bypass the issue with the property?

aydeisen avatar May 09 '18 12:05 aydeisen

@aydeisen I saw that method too. GetMembers() returns a different type PrincipalSearchResult<T> Class, a collection of Principal Class. Maybe that could be used. But right now it is not certain that GetUnderlyingObject() is the problem?

I'm curios what member it returns from the group that yields an empty name. If there are something special about that member?

johlju avatar May 09 '18 12:05 johlju

@johlju: sorry...I got sidetracked seeing double the number of disposables, and what appeared to be redundant objects.

I'll put a verbose statement into the function to see if I can get it to return the group that the members are being enumerated from.

Also (and I should have thought of this before), I'm going to modify the previous verbose outputs to include the SID. If the null values are coming from domain objects, then DSC wouldn't be able to read the "human readable" attributes like the name, which is what I was returning previously.

aydeisen avatar May 10 '18 13:05 aydeisen

No worries, all suggestions are good 🙂 Sounds good adding the additional verbose output.

johlju avatar May 10 '18 14:05 johlju

@johlju: I've been able to account for the null outputs for the Name property; I'm just having a little difficulty in correcting for it in the Verbose outputs.

The objects of type System.DirectoryServices.AccountManagement that are coming up with an empty are the local accounts that are members (dubbed both 'IrrelevantUserAccount0# and Member01 in previous outputs). The accounts have their sAMAccountName populated, but not the 'Name' property.

The Null output for the object of type System.DirectoryServices.AccountManagement.PrincipalContext is the local machine. The constructor being used in the Get-PrincipalContext function to get the local machine context is the one that only specifies the ContextType. Since Name isn't specified, it can't be returned.

That leaves the two objects of type System.DirectoryServices.AccountManagement.GroupPrincipal: the Administrators group and the Domain Admins group. However, I have no idea why neither are returning a property in the Name field. The Administrators group is the built-in local group, so I would assume it would have all of its properties. I expected the Domain Admins group to not return its name due to access, but its ADSI counterpart returned the name just fine. So, I have no explanation for why the Name property appears null for those two.

I'm attempting to return the sAMAccountName instead, but that was also coming up empty.

aydeisen avatar May 11 '18 20:05 aydeisen

I've been able to replicate the issue outside the context of DSC. It looks like the machine being managed by DSC is on a slow connection with no local DC, and the connection is unstable enough that activities against the domain controller occasionally timeout.

I do think there should be a better way to go about doing this, and that there's a lot of unnecessary convolution in the Group DSC resource. However, the specific error I was getting no longer appears to be related to it.

aydeisen avatar May 11 '18 21:05 aydeisen

@aydeisen If you can replicate this outside of DSC, can you find a better way of doing what the resource is doing, while keeping the intended behavior, and still handle a slow response from a domain controller? If so we could might incorporate a better method for doing this.

johlju avatar May 14 '18 13:05 johlju

@johlju: my first thought would be to use the IsMemberOf() Method of the UserPrincipal class for the IncludeMembers and ExcludeMembers when the Group Member is identified as a local account. The method returns True/False, and act accordingly. (i.e: for IncludeMembers, the expected result is 'True' and the member should be added if the result is False; for ExcludeMembers, the expected result is 'False', and the member should be removed if the result is True

Based on the note on line 41 of the module, the ADSI object is only required due to limitations in enumerating domain members accounts. The account is already identified whether it's local or domain in the Get-MembersAsPrincipalsList function on line 1636, and the GroupPrincipal and UserPrincipal objects required for that method already exist as part of the module. So, all the components needed to use the IsMemberOf() method of the UserPrincipal already exist in the module.

The biggest problem I can see is scalability, as this method would need to be executed on each member listed. In my case, where I'm only trying to confirm a single user as a member of a group, this would work great because I only need it to run once. If we're talking about arrays of multiple members, this may be time-consuming. It would also depend on whether the savings of both not contacting the domain controller and not getting the underlying ADSI object are enough offset the additional processing of each member. This may be a non-issue if this logic can be executed concurrently on each member instead of consecutively.

aydeisen avatar May 14 '18 14:05 aydeisen

Well, I yield on the ADSI part. The only way to add a member to a .NET Group Principal object is to call $GroupPrincipal.Members.Add(), which causes .NET to try to populate all the Group Members, and generate the connectivity error.

It does appear to be possible to add a member to a group without iterating through all the group members, using the below script as an example.

Add-Type -AssemblyName System.DirectoryServices.AccountManagement

$LocalCtx = [System.DirectoryServices.AccountManagement.PrincipalContext]::new([System.DirectoryServices.AccountManagement.ContextType]::Machine)
$GroupPrincipal = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($ctx, $GroupName)
$ADSIGroup = $GroupPrincipal.GetUnderlyingObject()

ForEach ($Member in $MembersToInclude)
{
    if ($Member -match '^\w+$')
    {
        $SearchCtx = $LocalCtx
    }
    elseif ($Member -match '\w+@\w+|\w+\\\w+|CN=\w+(?=.*DC=\w+)')
    {
        $SearchCtx = [System.DirectoryServices.AccountManagement.PrincipalContext]::new([System.DirectoryServices.AccountManagement.ContextType]::Domain)
    }
    else
    {
        Write-Error -Message 'Invalid Format for User Name'
    }

    $MemberPrincipal = [System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($SearchCtx, $Member)
    
    if ($MemberPrincipal.IsMemberOf($GroupPrincipal) -eq $True)
    {
        Write-Verbose -Message "$($Member.SamAccountName) already in $($GroupPrincipal.Name)"
    }
    else
    {
        $ADSIGroup.Add($MemberPrincipal.GetUnderlyingObject().Path)
    }
}

aydeisen avatar May 14 '18 18:05 aydeisen

@aydeisen what I understood was the connectivity problem you were having was in the Test-TargetResource? Test-TargetResource does not add any members. Do you have connectivity problems in the Set-TargetResource function as well? If we are discussing a new method of adding members to a group that is not related to this issue, then I suggest we move that discussion to a new issue. If this is related to this issue then let's continue the discussion here.

Have you incorporated that change above in the resource locally and the resource work as expected with the changes (no connectivity issues in the Test-TargetResurce function?)

johlju avatar May 15 '18 07:05 johlju