Object to hashtable key comparison - powershell

I'm looking for some help troubleshooting comparing .key values to objects.
Basically what's happening here is I'm connecting to two VMware vCenters and downloading a list of roles and putting those roles into two hash tables, then comparing them.
The problem comes down to the Process-Roles function where the comparing logic is flawed somewhere. It outputs all of the roles in both lists. I think the (-not .containskey) isn't working right. I've debugged in powerGUI and both hashtables and mstr_roles/slave_roles are all filled correctly.
The roles lists should be object lists, as they were filled with Get-VIRole.
The hash table should be object-in-key, value null lists. Is it possible to compare these two? I'm trying to check if the $role object in the roles list exists in the .key values list of the hash table.
$creds = Get-Credential
$mst = Read-Host "`n Master Server: "
$slv = Read-Host "`n Slave Server: "
$hsh_mstr_roles = #{}
$hsh_slave_roles = #{}
$mstr_roles = ""
$slave_roles = ""
Get-Roles -MasterServer $mst -SlaveServer $slv
Process-Roles
.
function Get-Roles() {
Param(
[Parameter(Mandatory=$True,Position=0)]
[string]$MasterServer,
[Parameter(Mandatory=$True,Position=1)]
[string]$SlaveServer
)
#Get Master Roles
Connect-VIServer $MasterServer -Credential $creds
$mstr_roles = Get-VIrole
foreach ($role in $mstr_roles) {
$hsh_mstr_roles.add($role, $null)
}
Disconnect-VIServer $MasterServer -Confirm:$false
#Get Slave Roles
Connect-VIServer $SlaveServer -Credential $creds
$slave_roles = Get-VIrole
foreach ($role in $slave_roles) {
$hsh_slave_roles.add($role, $null)
}
Disconnect-VIServer $SlaveServer -Confirm:$false
Write-Host "`n + Retrieved Roles Successfully"
}
.
function Process-Roles () {
#Get Roles on Master NOT ON SLAVE
Write-Host "`n"
foreach ($role in $mstr_roles){
if(-not $hsh_slave_roles.containsKey($role)){
Write-Host $role "doesn't exist on slave"
}
}
#Get Roles on Slave NOT ON MASTER
foreach ($role in $slave_roles){
if(-not $hsh_mstr_roles.containsKey($role)){
Write-Host $role "doesn't exist on master"
}
}
Write-Host "`n + Processed Roles Successfully"
}

The easiest way to do this is by finding the complement to one of the two sets of Keys that each hashtable has, using -notcontains:
function Process-Roles {
param(
[hashtable]$MasterRoles,
[hashtable]$SlaveRoles
)
# Complement to slave roles (those ONLY in $MasterRoles)
$MasterRoles.Keys |Where-Object { $SlaveRoles -notcontains $_ }|ForEach-Object {
Write-Host "$_ not in Slave Roles"
}
# and the other way around (those ONLY in $SlaveRoles)
$SlaveRoles.Keys |Where-Object { $MasterRoles -notcontains $_ }|ForEach-Object {
Write-Host "$_ not in Master Roles"
}
}
I'll have to add that your way of working with variables in different scopes is sub-optimal.
Define the parameters that the function needs in order to "do its job"
Return output from your functions where it make sense (any Get-* function should at least)
Depend on the Global and Script scopes as little as possible, preferably not at all
I would go with something like this instead:
Add a Credential parameter to the Get-Roles function and return the results rather than modifying a variable in a parent scope (here, using a Hashtable of role categories):
function Get-Roles {
Param(
[Parameter(Mandatory=$True,Position=0)]
[string]$MasterServer,
[Parameter(Mandatory=$True,Position=1)]
[string]$SlaveServer,
[Parameter(Mandatory=$True,Position=2)]
[pscredential]$Credential
)
$DiscoveredRoles = #{}
# Get Master Roles
Connect-VIServer $MasterServer -Credential $Credential
$DiscoveredRoles["MasterRoles"] = Get-VIRole
Disconnect-VIServer $MasterServer -Confirm:$false
#Get Slave Roles
Connect-VIServer $SlaveServer -Credential $Credential
$DiscoveredRoles["SlaveRoles"] = Get-VIrole
Disconnect-VIServer $SlaveServer -Confirm:$false
Write-Verbose "`n + Retrieved Roles Successfully"
return $DiscoveredRoles
}
Define parameters for the Process-Roles function, that match the hashtable you expect to generate from Get-Roles and do the same comparison of the role names as above, only this time we grab them directly from the Role objects:
function Process-Roles {
param(
[Parameter(Mandatory=$true)]
[ValidateScript({ $_.ContainsKey("MasterRoles") -and $_.ContainsKey("SlaveRoles") })]
[hashtable]$RoleTable
)
$MasterRoleNames = $RoleTable["MasterRoles"] |Select-Object -ExpandProperty Name
$SlaveRoleNames = $RoleTable["SlaveRoles"] |Select-Object -ExpandProperty Name
$MasterRoleNames |Where-Object { $SlaveRoleNames -notcontains $_ } |ForEach-Object {
Write-Host "$_ doesn't exist on slave"
}
$SlaveRoleNames |Where-Object { $MasterRoleNames -notcontains $_ } |ForEach-Object {
Write-Host "$_ doesn't exist on Master"
}
Write-Host "`n + Processed Roles Successfully"
}
Update your executing script with the new parameters:
$creds = Get-Credential
$MasterServer = Read-Host "`n Master Server: "
$SlaveServer = Read-Host "`n Slave Server: "
$RoleTable = Get-Roles -MasterServer $MasterServer -SlaveServer $SlaveServer -Credential $creds
Process-Roles -RoleTable $RoleTable
Next step would be to add pipeline support to the Process-Roles function, converting Write-Host statements to Write-Verbose and adding error handling, but I'll leave that as an exercise to OP :-)

try:
if(!$hsh_slave_roles.containsKey($role))

Related

Confirming before proceeding through checking of services on multiple machines

My goal is to check the services on multiple remote machines to make sure they are running, and starting them if they are not. I would like to modify the below code to add the ability to ask the user before proceeding to the next $computerName, to display and confirm the status of group of $serviceNames that is being passed through the function.
In a text file servers.txt the contents are as follows:
server1-serviceA,ServiceB,ServiceC
server2-serviceD,ServiceE,ServiceF
server3-serviceG,ServiceH,ServiceI
And here is the powershell script, checking different services for each server using the split function
$textFile = Get-Content C:\temp\servers.txt
foreach ($line in $textFile) {
$computerName = $line.split("-")[0] #Getting computername by using Split
$serviceNames = $line.split("-")[1] #Getting Service names by using split
foreach ($serviceName in $serviceNames.split(",")) {
# Again using split to handle multiple service names
try {
Write-Host " Trying to start $serviceName in $computerName"
Get-Service -ComputerName $computerName -Name $serviceName | Start-Service -ErrorAction Stop
Write-Host "SUCCESS: $serviceName has been started"
}
catch {
Write-Host "Failed to start $serviceName in $computerName"
}
}
}
You could use the PSHostUserInterface.PromptForChoice method for this, here is an example of how you can implement it:
# Clear this variable before in case it's already populated
$choice = $null
foreach ($line in $textFile) {
$computerName = $line.split("-")[0]
$serviceNames = $line.split("-")[1]
# If previous choice was not equal to 2 (Yes to All), ask again
if($choice -ne 2) {
$choice = $host.UI.PromptForChoice(
"my title here", # -> Title
"Continue with: ${computerName}?", # -> Message
#("&Yes", "&No", "Yes to &All"), # -> Choices
0 # -> Default Choice (Yes)
)
}
# If choice was 1 (No), stop the script here
if($choice -eq 1) { return }
# Same logic here
}

Powershell Printing Custom per Printer Permissions

First off I would like to thank everyone for helping me work thru my issue.
Scope:
I am looking to write a script that will dynamically build the full set of permissions for each printer. As each printer has it's own Dynamic Group and is not allowed to have the everyone group applied to the printer.
Example:
Printer Name: PrinterA
AdGroup for Printer: gprt_PrinterA
Other groups assigned full (Print/Manage Doc/Manage Printer) permissions to the printer : Local Admin/Local Power User/Local Print Operator/Network Admins (Domain Group)
Other groups with Manage Documents and Print permissions to the printer: Endpoint (Domain Group)/Service Desk (Domain Group)/gprt_PrinterA (Domain Group)\
First what works and I see many examples about this across the web but does not meet my requirements:
$DefaultPrinterInfo = Get-Printer -Name PrinterA -Full
Set-Printer -Name PrinterB -PermissionSDDL ($DefaultPrinterInfo.PermissionSDDL)
IMPORTANT:
This however does not work to meet the required specifications. The reason is the gprt_PrinterA group can not exist on PrinterB. PrinterB must have the gprt_PrinterB Group.
In one example I have attempted to:
Set-Printer -Name PrinterB -PermissionSDDL "G:SYD:(A;;LCSWSDRCWDWO;;;BA)(A;OIIO;RPWPSDRCWDWO;;;BA)"
I have attempted to even dynamically create the default permission groups required and if this worked then it would be easy for me to just add 1 more group that is dynamically assigned:
(A;;LCSWSDRCWDWO;;;BA)(A;OIIO;RPWPSDRCWDWO;;;BA)
(A;;LCSWSDRCWDWO;;;PU)(A;OIIO;RPWPSDRCWDWO;;;PU)
(A;;LCSWSDRCWDWO;;;PO)(A;OIIO;RPWPSDRCWDWO;;;PO)
(A;;LCSWSDRCWDWO;;;S-1-5-21-51083937-621610274-1850952788-69794)(A;OIIO;RPWPSDRCWDWO;;;S-1-5-21-51083937-621610274-1850952788-69794)
(A;CIIO;RC;;;S-1-5-21-51083937-621610274-1850952788-69792)(A;OIIO;RPWPSDRCWDWO;;;S-1-5-21-51083937-621610274-1850952788-69792)(A;;SWRC;;;S-1-5-21-51083937-621610274-1850952788-69792)
(A;CIIO;RC;;;S-1-5-21-51083937-621610274-1850952788-69791)(A;OIIO;RPWPSDRCWDWO;;;S-1-5-21-51083937-621610274-1850952788-69791)(A;;SWRC;;;S-1-5-21-51083937-621610274-1850952788-69791)
I kept the groups clean for easy reading but essentially just make it a continuous line with "G:SYD:" in the beginning. Then replace the PermissionSDDL in the above powershell statement. Either way though, I keep getting the error: "[Set-Printer : Access was denied to the specific resource]"
I have even attempted to do the following:
SetSecurityDescriptor method of the Win32_Printer class
Set-PrinterPermission.ps1
The Security Descriptor Definition Language of Love (Part 2)
Adding Multiple Permissions to a Share
These did put me on the correct path! It lets me replace the permission on the printer. But it strips all existing permission, putting on only the single permission specified for the printer. I need to apply a whole set of permissions to the printer as you see above. I am a little out of my realm but learning how to build a Multi-ACL Package to apply to the printer.
I am ok with replacing all permissions, if I can assign a whole set of permissions, or simply add and remove to the existing permissions if they do or not exist.
What I have learned in my research the permission sets need to be:
Print/Manage this Printer
# G:SYD:(A;;LCSWSDRCWDWO;;;$SID)
Print
# G:SYD:(A;;SWRC;;;$SID)
Print/Manage this Printer/Manage Documents/Special Permissions
# G:SYD:(A;;LCSWSDRCWDWO;;;$SID)(A;OIIO;RPWPSDRCWDWO;;;$SID)
I hope someone the help me figure out a solution please.
Ok so after extensively researching I am getting closer.
The "Set-PrinterPermission" script is on the correct path. What I have had to do, is stripped out the ACE function from the script to place it into it's own function.
function New-PrinterACE
{
##[CmdletBinding(SupportsShouldProcess)]
Param (
[Parameter(
Mandatory = $true,
HelpMessage = "User/group to grant permissions"
)]
[String]$UserName,
[Parameter(
Mandatory = $true,
HelpMessage = "Permissions to apply"
)]
[ValidateSet('Takeownership', 'ReadPermissions', 'ChangePermissions', 'ManageDocuments', 'ManagePrinters', 'Print + ReadPermissions')]
[String]$Permission,
[Parameter(
Mandatory = $true,
HelpMessage = "Permissions to apply"
)]
[ValidateSet('Allow', 'Deny', 'System Audit')]
[String]$AccessType
)
$Ace = ([WMIClass] "Win32_Ace").CreateInstance()
$Trustee = ([WMIClass] "Win32_Trustee").CreateInstance()
Write-Verbose "Translating UserName (user or group) to SID"
$SID = (New-Object security.principal.ntaccount $UserName).translate([security.principal.securityidentifier])
Write-Verbose "Get binary form from SID and byte Array"
[byte[]]$SIDArray = , 0 * $SID.BinaryLength
$SID.GetBinaryForm($SIDArray, 0)
Write-Verbose "Fill Trustee object parameters"
$Trustee.Name = $UserName
$Trustee.SID = $SIDArray
Write-Verbose "Translating $Permission to the corresponding Access Mask"
Write-Verbose "Based on https://learn.microsoft.com/en-US/windows/win32/cimwin32prov/setsecuritydescriptor-method-in-class-win32-printer?redirectedfrom=MSDN"
Write-Verbose "https://social.technet.microsoft.com/Forums/Windows/en-US/a67e3ffd-5e41-4e2f-b1b9-c7c2f29a3a12/adding-permissions-to-an-existing-share"
switch ($Permission)
{
'Takeownership'
{
$Ace.AccessMask = "524288"
}
'ReadPermissions'
{
$Ace.AccessMask = "131072"
}
'ChangePermissions'
{
$Ace.AccessMask = "262144"
}
'ManageDocuments'
{
$Ace.AccessMask = "983088"
}
'ManagePrinters'
{
$Ace.AccessMask = "983052"
}
'Print + ReadPermissions'
{
$Ace.AccessMask = "131080"
}
}
Write-Verbose "Translating $AccessType to the corresponding numeric value"
Write-Verbose "Based on https://learn.microsoft.com/en-US/windows/win32/cimwin32prov/setsecuritydescriptor-method-in-class-win32-printer?redirectedfrom=MSDN"
switch ($AccessType)
{
"Allow"
{
$Ace.AceType = 0
$Ace.AceFlags = 0
}
"Deny"
{
$Ace.AceType = 1
$Ace.AceFlags = 1
}
"System Audit"
{
$Ace.AceType = 2
$Ace.AceFlags = 2
}
}
Write-Verbose "Write Win32_Trustee object to Win32_Ace Trustee property"
$Ace.Trustee = $Trustee
Return $ACE
}
$MyPrinterAces = #()
$MyPrinterAces += New-PrinterACE -UserName <DomainUserA> -Permission ManagePrinters -AccessType Allow
$MyPrinterAces += New-PrinterACE -UserName <DomainUserA> -Permission ManageDocuments -AccessType Allow
$MyPrinterAces += New-PrinterACE -UserName "DomainGroupA" -Permission ManageDocuments -AccessType Allow
$MyPrinterAces += New-PrinterACE -UserName "DomainGroupA" -Permission 'Print + ReadPermissions' -AccessType Allow
#https://learn.microsoft.com/en-us/windows/win32/wmisdk/wmi-security-descriptor-objects#example-checking-who-has-access-to-printers
#https://stackoverflow.com/questions/60261292/explicit-access-array-from-acl-win32-api
This, with a few other cosmetic modifications to the "Set-PrinterPermission" script to accommodate; So that it now references this function to build the ACE's it uses and to add the ability for it to accommodate an array of multiple users/groups with permissions types.
function Set-PrinterPermission
{
[CmdletBinding(SupportsShouldProcess)]
Param (
[Parameter(
Mandatory = $true,
HelpMessage = "Server or array of servers",
ParameterSetName = 'OnePrinter'
)]
[Parameter(
Mandatory = $true,
HelpMessage = "Server or array of servers",
ParameterSetName = 'AllPrinters'
)]
[string[]]$Servers,
[Parameter(
HelpMessage = "Name of the Printer",
ParameterSetName = 'OnePrinter'
)]
[String]$PrinterName,
$PrinterPermissions =
#(
#('Administrators', 'ManagePrinters','Allow'),
#('Power Users', 'ManagePrinters','Allow'),
#('Print Operators', 'ManagePrinters','Allow'),
#('OHD – Network Support Team', 'ManagePrinters','Allow'),
#("OHD – PC Support Team", 'Print + ReadPermissions','Allow'),
#("OHD - Service Desk Users", 'Print + ReadPermissions','Allow')
)
)
Begin
{
$greenCheck =
#{
Object = [Char]8730
ForegroundColor = 'Green'
NoNewLine = $true
}
ConvertFrom-SddlString -Sddl $printer.PermissionSDDL
#Write-Host "Status check... " -NoNewline
#Start-Sleep -Seconds 1
#Write-Host #greenCheck
#Write-Host " (Done)"
Write-Output "Beginning Treatment ..."
Write-Verbose "creating instances of necessary classes ..."
$SD = ([WMIClass] "Win32_SecurityDescriptor").CreateInstance()
$Aces = #()
Foreach ($PrinterPermission in $PrinterPermissions)
{
$Aces += New-PrinterACE -UserName $PrinterPermission[0] -Permission $PrinterPermission[1] -AccessType $PrinterPermission[2]
}
Write-Verbose "Write Win32_Ace and Win32_Trustee objects to SecurityDescriptor object"
$SD.DACL = $Aces
Write-Verbose "Set SE_DACL_PRESENT control flag"
$SD.ControlFlags = 0x0004
}
process
{
try
{
If ($PSCmdlet.ParameterSetName -eq "OnePrinter")
{
ForEach ($Server in $Servers)
{
$Printer = Get-Printer -ComputerName $Server -Name $PrinterName -ErrorAction Stop
$PrinterName = $Printer.name
Write-Output "Beginning treatment of: $PrinterName On: $Server"
Write-Verbose "Get printer object"
<#
It seems that i can't use the Filter parameter using a var
$PrinterWMI = Get-WMIObject -Class WIN32_Printer -Filter "name = $PrinterName"
I've also noticed that I've haven't the same result using Get-CimInstance in particular with
$PrinterCIM.psbase.scope
However I'm sure that using Get-CiMInstance will be better, but i don't know how to proceed
then I'm using the following "Legacy" approach
https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/the-security-descriptor-definition-language-of-love-part-1/ba-p/395202
https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/the-security-descriptor-definition-language-of-love-part-2/ba-p/395258
http://docs.directechservices.com/index.php/category-blog-menu/319-the-security-descriptor-definition-language-of-love
https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-strings?redirectedfrom=MSDN
https://learn.microsoft.com/en-us/windows/win32/secauthz/access-tokens
#>
#$PrinterWMI = (Get-WmiObject -Class WIN32_Printer | Where-Object -FilterScript { $_.Name -like "wilpa0p11" }).GetSecurityDescriptor().Descriptor.dacl
$PrinterWMI = Get-WmiObject -Class WIN32_Printer | Where-Object -FilterScript { $_.Name -like $PrinterName }
Write-Verbose "Enable SeSecurityPrivilege privilegies"
$PrinterWMI.psbase.Scope.Options.EnablePrivileges = $true
Write-Verbose "Invoke SetSecurityDescriptor method and write new ACE to specified"
$PrinterWMI.SetSecurityDescriptor($SD)
Write-Verbose "Treatment of $PrinterName : Completed"
}
} # end if OnePrinter Parameter Set
If ($PSCmdlet.ParameterSetName -eq "AllPrinters")
{
ForEach ($Server in $Servers)
{
$Printers = Get-Printer -ComputerName $Server | Where-Object { $_.Shared -eq $true } -ErrorAction Stop
ForEach ($Printer in $Printers)
{
$PrinterName = $Printer.name
Write-Output "Beginning treatment of : $PrinterName"
Write-Verbose "Get printer object"
<#
It seems that i can't use the Filter parameter using a var
$PrinterWMI = Get-WMIObject -Class WIN32_Printer -Filter "name = $PrinterName"
I've also noticed that I've haven't the same result using Get-CimInstance in particular with
$Printer.psbase.scope
then I'm using the following approach
However I'm sure that using Get-CiMInstance will be better
#>
$PrinterWMI = Get-WmiObject -Class WIN32_Printer | Where-Object -FilterScript { $_.Name -like $PrinterName }
Write-Verbose "Enable SeSecurityPrivilege privilegies"
$PrinterWMI.psbase.Scope.Options.EnablePrivileges = $true
Write-Verbose "Invoke SetSecurityDescriptor method and write new ACE to specified"
$PrinterWMI.SetSecurityDescriptor($SD)
Write-Output "Treatment of $PrinterName : Completed"
}
}
} # end if All Printers Parameter Set
} # End Try
catch
{
Write-Error "Hoops an error occured"
Write-Error $_.Exception.Message
}
}
end
{
Write-Output "All treatments : completed"
}
} # end function
Now this is working great I can easily add the dynamic group as a parameter and a ACE will get assigned to the security descriptor of the printer.
Now my problem is I am unable to add the "Manage Documents" permission to the printer. if anyone can help me with this I will have my project complete.
The permission is assigned correctly for Printing only, and Manage Printer.
Primary Issue needing help resolving:
I am so very close now... what am I doing wrong to apply the "Manage Documents" permission to the printer ACL?
The Image below is the results of the script trying to apply the "Manage Documents" Permissions.
Very Minor Cosmetic help:
is there a way to validate the $PrinterPermissions in the Parameters section of the code? My thinking is to validate the parameter in the begin section of the code and exit out if one of my validations fail. not sure if there is a better way.

Can I set the "-Server" parameter for use with Active Directory cmdlets per PS session?

I'm administering three domains (let's call them xx.company.local, xy.company.local, and xz.company.local) and I've got domain admin accounts for all three domains.
If I run powershell as my xx domain admin user I don't need to specify the -Server parameter for AD cmdlets, but I do when running PS as the other two. I assume this is because my PC is only joined to the xx domain.
Is there a command that I can run when opening up the xy and xz powershell sessions that will set that -Server parameter? (I'm looking into $PSHome and $PSDefaultParameterValues, but figured I would ask before wasting too much time.)
Extra thought: I have three runas shortcuts for my ADUC gui (mmc.exe %SystemRoot%\system32\dsa.msc) and the one-time single-step for the xy and xz domains was to change the domain. I'm hoping that there's a similar step I can run for the powershell side of things.
So I was able to modify my script since nobody posted an answer.
first, I created a global variable in my .psm1 file
$domain_controller = Get-ADDomainController -Discover -Service PrimaryDC
then, I created a new function
function Set-DomainController {
[CmdletBinding()]
Param(
[ValidateSet(“xx”, ”xy”, ”xz”)]
[Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[string]
$xxxyxz
)
process {
if ($PSBoundParameters.ContainsKey('xxxyxz')) {
$script:domain_controller = Get-ADDomainController -Discover -Service PrimaryDC -Domain "$xxxyxz.company.local"
}
else {
Write-Output ""
Write-Output "You are currently using " $script:domain_controller.Hostname
Write-Output ""
Write-Output "Select a domain:"
Write-Output "[1] - xx.company.local"
Write-Output "[2] - xy.company.local"
Write-Output "[3] - xz.company.local"
Write-Output ""
$selection = Read-Host "Please make a selection"
switch ($selection) {
'1' {
$script:domain_controller = Get-ADDomainController -Discover -Service PrimaryDC -Domain xx.company.local
}
'2' {
$script:domain_controller = Get-ADDomainController -Discover -Service PrimaryDC -Domain xy.company.local
}
'3' {
$script:domain_controller = Get-ADDomainController -Discover -Service PrimaryDC -Domain xzcompany.local
}
}
Write-Output ""
Write-Output "You are currently using " $script:domain_controller.Hostname
Write-Output ""
}
}
}
Set-Alias -Name setdc -Value Set-DomainController
lastly I went through the rest of the functions in my module and added -Server $script:domain_controller to every Get-AD* call throughout
took a few hours, but now it's done
... still looking for something better ...

Get-ADPrincipalGroupMembership An unspecified error has occurred

I am getting errors with Get-ADPrincipalGroupMembership command on Windows 10 (x64) machine. I have installed the required RSAT- 'Active directory Domain service and Lightweight Directory service tools' and 'Server manager' dependencies as specified int this document. I am able to execute Get-AdUser and see the results but Get-ADPrincipalGroupMembership is throwing below error.
PS C:\Users\JYOTHI> Get-ADPrincipalGroupMembership jyothi
Get-ADPrincipalGroupMembership : An unspecified error has occurred
At line:1 char:1
+ Get-ADPrincipalGroupMembership gapalani
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (jyothi:ADPrincipal) [Get-ADPrincipalGroupMembership], ADException
+ FullyQualifiedErrorId : ActiveDirectoryServer:0,Microsoft.ActiveDirectory.Management.Commands.GetADPrincipalGroupMembership
I can try the other way
(Get-Aduser jyothi -Properties MemberOf | Select MemberOf).MemberOf
but like to know what is the fix for Get-ADPrincipalGroupMembership
As you have noticed, Get-ADPrincipalGroupMembership fails with an obscure error if the reference object's name contains certain characters, or if it's a member of one or more groups that contain certain characters in their names.
I don't have definitive proof, but my testing indicates that the underlying issue is that Get-ADPrincipalGroupMembership, internally, uses ADSI and fails to correctly escape distinguished names that contain characters that need to be escaped. (If this is the case, Microsoft should be using the IADsPathname interface to escape names correctly. This would be an embarrassing oversight on their part.)
Unfortunately, this problem renders the cmdlet broken and unusable in production environments.
Here's a relatively short PowerShell script that doesn't suffer from this annoyance and also supports retrieving recursive group memberships:
# Get-ADGroupMembership.ps1
# Written by Bill Stewart
#requires -version 2
# Version history:
# 1.0 (2019-12-02)
# * Initial version. Only searches the current domain.
<#
.SYNOPSIS
Gets the distinguished names of the Active Directory groups that have a specified object as a member.
.DESCRIPTION
Gets the distinguished names of the Active Directory groups that have a specified object, represented by the -Identity parameter, as a member.
.PARAMETER Identity
Specifies an Active Directory object. You can specify either the distinguishedName or the sAMAccountName of the object.
.PARAMETER Recursive
Specifies to include the object's nested group memberships.
.NOTES
If you use the ActiveDirectory PowerShell module and want Microsoft.ActiveDirectory.Management.ADGroup objects as output, pipe this command's output to the Get-ADGroup cmdlet.
.EXAMPLE
Get the distinguished names of the groups that the kendyer account is a member of:
PS C:\> Get-ADGroupMembership kendyer
.EXAMPLE
Get the distinguished names of the groups that the kendyer account is a member of, including nested groups:
PS C:\> Get-ADGroupMembership kendyer -Recursive
.EXAMPLE
Get the ADGroup objects representing the groups that the kendyer account is a member of (requires the Active Directory module):
PS C:\> Get-ADGroupMembership kendyer | Get-ADGroup
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true,ValueFromPipeline = $true)]
[String[]] $Identity,
[Switch] $Recursive
)
begin {
$CommandName = $MyInvocation.MyCommand.Name
# Set up Pathname COM object
$ADS_ESCAPEDMODE_ON = 2
$ADS_SETTYPE_DN = 4
$ADS_FORMAT_X500_DN = 7
$Pathname = New-Object -ComObject "Pathname"
if ( -not $Pathname ) {
return
}
[Void] $Pathname.GetType().InvokeMember("EscapedMode","SetProperty",$null,$Pathname,$ADS_ESCAPEDMODE_ON)
# Outputs correctly escaped distinguished name using Pathname object
function Get-EscapedName {
param(
[String] $distinguishedName
)
[Void] $Pathname.GetType().InvokeMember("Set","InvokeMethod",$null,$Pathname,#($distinguishedName,$ADS_SETTYPE_DN))
$Pathname.GetType().InvokeMember("Retrieve","InvokeMethod",$null,$Pathname,$ADS_FORMAT_X500_DN)
}
# Outputs the memberOf attribute of an object using paged search (in case
# an object is a member of a large number of groups)
function Get-MemberOfAttribute {
param(
[String] $distinguishedName,
[Ref] $memberOf,
[Switch] $recursive
)
$searcher = [ADSISearcher] "(objectClass=*)"
$searcher.SearchRoot = [ADSI] "LDAP://$(Get-EscapedName $distinguishedName)"
$lastQuery = $false
$rangeStep = 1500
$rangeLow = 0
$rangeHigh = $rangeLow + ($rangeStep - 1)
do {
if ( -not $lastQuery ) {
$property = "memberOf;range={0}-{1}" -f $rangeLow,$rangeHigh
}
else {
$property = "memberOf;range={0}-*" -f $rangeLow
}
$searcher.PropertiesToLoad.Clear()
[Void] $searcher.PropertiesToLoad.Add($property)
$searchResults = $searcher.FindOne()
if ( $searchResults.Properties.Contains($property) ) {
foreach ( $searchResult in $searchResults.Properties[$property] ) {
if ( $memberOf.Value.Count -gt 100 ) {
Write-Progress `
-Activity $CommandName `
-Status "Getting membership of '$distinguishedName'" `
-CurrentOperation $searchResult
}
if ( $recursive ) {
if ( -not $memberOf.Value.Contains($searchResult) ) {
Get-MemberOfAttribute $searchResult $memberOf -recursive
}
}
if ( -not $memberOf.Value.Contains($searchResult) ) {
$memberOf.Value.Add($searchResult)
}
}
$done = $lastQuery
}
else {
if ( -not $lastQuery ) {
$lastQuery = $true
}
else {
$done = $true
}
}
if ( -not $lastQuery ) {
$rangeLow = $rangeHigh + 1
$rangeHigh = $rangeLow + ($rangeStep - 1)
}
}
until ( $done )
Write-Progress `
-Activity $CommandName `
-Status "Getting membership of '$distinguishedName'" `
-Completed:$true
}
function Get-ADGroupMembership {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[String] $identity,
[Switch] $recursive
)
$ldapString = $identity -replace '\\','\5c' -replace '\(','\28' -replace '\)','\29' -replace '\*','\2a' -replace '\/','\2f'
$searcher = [ADSISearcher] "(|(distinguishedName=$ldapString)(sAMAccountName=$ldapString))"
try {
$searchResults = $searcher.FindAll()
if ( $searchResults.Count -gt 0 ) {
foreach ( $searchResult in $searchResults ) {
$memberOf = New-Object Collections.Generic.List[String]
Get-MemberOfAttribute $searchResult.Properties["distinguishedname"][0] ([Ref] $memberOf) -recursive:$recursive
$memberOf
}
}
else {
Write-Error "Cannot find an object with identity '$identity'." -Category ObjectNotFound
}
}
catch {
Write-Error -ErrorRecord $_
}
finally {
$searchResults.Dispose()
}
}
}
process {
foreach ( $IdentityItem in $Identity ) {
Get-ADGroupMembership $IdentityItem -recursive:$Recursive
}
}
I've also added this script as a public gist on github in case something needs fixing or if I add new features.
Get-ADPrincipalGroupMembership -Identity "jyothi"

How to correctly pass a string array as a parameter in a PowerShell 4.0 Invoke-Command -ScriptBlock

I am working with PowerShell 4.0 and I am trying to pass a string array as one of the parameters for an Invoke-Command -ScriptBlock in which I am calling another PowerShell script on a remote server. When I do this, the string array seems to get flattened so that it appears as a single string value, rather than a string array.
Listed below is the 1st script, which is being called by a Bamboo deployment server that provides the initial parameters.
In the Debug section, the $SupportFolders string array is iterated by the FlowerBoxArrayText function and it properly writes the two folder paths to the console, as expected.
24-Oct-2017 14:59:33 *****************************************************************************
24-Oct-2017 14:59:33 **** E:\SRSFiles\SRSOutput
24-Oct-2017 14:59:33 **** E:\SRSFiles\SRSBad
24-Oct-2017 14:59:33 *****************************************************************************
Here is the initial part of the 1st script file, showing the input parameters, the string array creation and where I am calling the remote script via Invoke-Command;
[CmdletBinding(DefaultParametersetName='None')]
param (
# Allows you to specify Install, Delete or Check.
[ValidateSet("Install", "Delete", "Check")][string] $Action = "Check",
# Allows you to specify the remote server name.
[string] $ComputerName = "None",
# Allows you to specify the username to use for installing the service.
[string] $Username = "None",
# Allows you to specify the password to use for installing the service.
[string] $Password = "None",
# Allows you to specify the location of the support folders for the service, if used.
[string] $SupportFoldersRoot = "None"
)
Function CreateCredential()
{
$Pass = $Password | ConvertTo-SecureString -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential($Username, $Pass)
Return $Cred
}
Function FlowerBoxArrayText($TextArray, $TextColor="Yellow")
{
Write-Host "*****************************************************************************" -ForegroundColor $TextColor
foreach($TextLine in $TextArray)
{
IndentedText $TextLine $TextColor
}
Write-Host "*****************************************************************************" -ForegroundColor $TextColor
}
Function IndentedText($TextToInsert, $TextColor="Yellow")
{
Write-Host "**** $TextToInsert" -ForegroundColor $TextColor
}
$Credential = CreateCredential
[string[]] $ResultMessage = #()
[string] $Root = $SupportFoldersRoot.TrimEnd("/", "\")
[string[]] $SupportFolders = #("$Root\SRSOutput", "$Root\SRSBad")
#Debug
Write-Host "**** Starting debug in ManageAutoSignatureProcessorService ****"
FlowerBoxArrayText $SupportFolders -TextColor "Green"
Write-Host "**** Ending debug in ManageAutoSignatureProcessorService ****"
#End Debug
$ResultMessage = Invoke-Command -ComputerName $ComputerName -Credential $Credential -ScriptBlock {
param($_action,$_username,$_password,$_supportFolders) &"C:\Services\ManageService.ps1" `
-Action $_action `
-ComputerName DEV `
-Name DevProcessor `
-DisplayName 'DevProcessor' `
-Description 'DevProcessor' `
-BinaryPathName C:\Services\DevProcessor.exe `
-StartupType Manual `
-Username $_username `
-Password $_password `
-ServicePathName C:\Services `
-SupportFolders $_supportFolders `
-NonInteractive } -ArgumentList $Action,$Username,$Password,(,$SupportFolders)
if ($ResultMessage -like '*[ERROR]*')
{
FlowerBoxArrayText $ResultMessage -textColor "Red"
}
else
{
FlowerBoxArrayText $ResultMessage -textColor "Green"
}
Then, in the ManageService.ps1 script file on the remote server, I have the following;
[CmdletBinding(DefaultParametersetName='None')]
param (
# Allows you to specify Install, Delete or Check.
[ValidateSet("Install", "Delete", "Check")][string] $Action = "Check",
# Allows you to specify the name of the remote computer.
[string] $ComputerName = "None",
# Allows you to specify the service name.
[string] $Name = "None",
# Allows you to specify the service display name.
[string] $DisplayName = "None",
# Allows you to specify the service description.
[string] $Description = "None",
# Allows you to specify the path to the binary service executable file.
[string] $BinaryPathName = "None",
# Allows you to specify how the service will start, either manual or automatic.
[ValidateSet("Manual", "Automatic")][string] $StartupType = "Manual",
# Allows you to specify the domain username that the service will run under.
[string] $Username = "None",
# Allows you to specify the password for the domain username that the service will run under.
[string] $Password = "None",
# Allows you to specify the path to the service install scripts and service files on the remote server.
[string] $ServicePathName = "None",
# Allows you to specify the location of the support folders for the service, if used. The default value is an empty array
[string[]] $SupportFolders = #(),
# Disables human interaction, and allows all tests to be run even if they 'fail'.
[switch] $NonInteractive
)
Function CreateCredential()
{
$Pass = $Password | ConvertTo-SecureString -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential($Username, $Pass)
Return $Cred
}
[bool] $OkToInstall = $False
[string[]] $ResultMessage = #()
#Debug
$ResultMessage = $ResultMessage += "[DEBUG] ***************************************"
$ResultMessage = $ResultMessage += "[DEBUG] SupportFolders: [$SupportFolders] ."
foreach ($Folder in $SupportFolders)
{
$ResultMessage = $ResultMessage += "[DEBUG] SupportFolders Item: $Folder."
}
$Count = #($SupportFolders).Count
$ResultMessage = $ResultMessage += "[DEBUG] SupportFolders Count: $Count ."
$ResultMessage = $ResultMessage += "[DEBUG] ***************************************"
#End Debug
The line,
$ResultMessage = $ResultMessage += "[DEBUG] SupportFolders: [$SupportFolders] ."
shows the following result from the $ResultMessage value that is returned to the calling script;
**** [DEBUG] SupportFolders: [E:\SRSFiles\SRSOutput E:\SRSFiles\SRSBad] .
Notice that the array is flattened out.
The foreach loop that follows also only prints out one value instead of two;
"E:\SRSFiles\SRSOutput E:\SRSFiles\SRSBad"
I have spent considerable time researching a solution but have yet to find an answer.
Any ideas?
EDIT 1 using #Bacon Bits suggestion;
$Options = #{'Action' = $Action
'ComputerName' = 'DEV'
'Name' = 'DevProcessor'
'DisplayName' = 'DevProcessor'
'Description' = 'Generate daily processes'
'BinaryPathName' = 'C:\Services\DevProcessor\DevProcessor.exe'
'StartupType' = 'Manual'
'Username' = $Username
'Password' = $Password
'ServicePathName' = 'C:\Services\DevProcessor'
'SupportFolders' = $SupportFolders
}
$ScriptBlock = {
param($Options)
& {
param(
$Action,
$ComputerName,
$Name,
$DisplayName,
$Description,
$BinaryPathName,
$StartupType,
$Username,
$Password,
$ServicePathName,
$SupportFolders,
$NonInteractive
)
&powershell "C:\Services\DevProcessor\ManageService.ps1 $Action $ComputerName $Name $DisplayName $Description $BinaryPathName $StartupType $Username $Password $ServicePathName $SupportFolders"
} #Options;
}
$ResultMessage = Invoke-Command -ComputerName $ComputerName -Credential $Credential -ScriptBlock $ScriptBlock -ArgumentList $Options
If I run the code modified as it is listed above, I still get the flattened array for $SuppportFolders and the ManageService.ps1 script trips up over parameters that have spaces, even though they are quoted when I assign them.
The option to completely wrap the code in ManageService.ps1, as opposed to simply calling the remote script is not really viable because the ManagedService.ps1 script is fairly extensive and generic so I can call it from over 30 automation scripts in my deployment server.
I believe what #Bacon Bits is suggesting would work if it was feasible to wrap the ManageService script.
To pass a single array, you can do this:
Invoke-Command -Session $Session -ScriptBlock $ScriptBlock -ArgumentList (,$Array);
However, that only works if you only need to pass a single array. It can all fall apart as soon as you start to pass multiple arrays or multiple complex objects.
Sometimes, this will work:
Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList (, $Array1), (, $Array2), (, $Array3);
However, it can be inconsistent in my experience. Sometimes it flattens the arrays out again.
What you can do is something similar to this answer.
{param($Options)& <# Original script block (including {} braces)#> #options }
Basically what we do is:
Wrap the script in a scriptblock that accepts a single hashtable as an argument.
Put all our arguments into the hashtable.
Use the passed hashtable as a splat variable.
So it would be something like:
$Options = #{
Action = 'Check';
ComputerName = 'XYZ123456';
Name = 'MyName';
.
.
.
}
$ScriptBlock = {
param($Options)
& {
[CmdletBinding(DefaultParametersetName='None')]
param (
# Allows you to specify Install, Delete or Check.
[ValidateSet("Install", "Delete", "Check")][string] $Action = "Check",
# Allows you to specify the name of the remote computer.
[string] $ComputerName = "None",
# Allows you to specify the service name.
[string] $Name = "None",
.
.
.
.
#End Debug
} #Options;
}
Invoke-Command -ComputerName RemoteServer -ScriptBlock $ScriptBlock -ArgumentList $Options;
Here's a trivial working example:
$Options = #{
List1 = 'Ed', 'Frank';
List2 = 5;
List3 = 'Alice', 'Bob', 'Cathy', 'David'
}
$ScriptBlock = {
param($Options)
& {
param(
$List1,
$List2,
$List3
)
"List1"
$List1
''
"List2"
$List2
''
"List3"
$List3
} #Options;
}
Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $Options;
Output:
List1
Ed
Frank
List2
5
List3
Alice
Bob
Cathy
David
Note that I tested this on PowerShell v5. I no longer have a system with PowerShell v4 to test on.