Set up non-admin account to access WMI and performance data remotely with PowerShell
I am working in a project that want to get performance data from remote servers, this with WMI, the servers belong to an Active Directory but the user collecting is not allowed to be an administrator (As an Administrator this is easy because then you already are in control and in the right groups)
So How do I fix this with preferably PowerShell, Scripting Guy has made a post about how to add users to local groups and Steve Lee on the MSDN WMI Blog has made a post how to add permissions on WMI so I have combined them and got the solution I want.
The groups I am interested in on the remote computer are “Distributed COM Users”,”Performance Monitor Users”, to be able to connect and get monitoring data
To get it from the WMI interface I need some permissions there, I only grant read permissions on the root/CIMv2 subtree.
And here is the Powershell that does it, In the screendump you can see that I can use a foreach loop to set the permissions on several computers
# Create Permissions for non-admin user on remote computers # # Niklas Akerlund / 2012-08-22 Param ([switch]$add, [switch]$remove, $ComputerName = "vc", $UserName = "olle", $DomainName = "vniklas") # add functions to call . .\Set-UserLocalGroup.ps1 . .\Set-WmiNamespaceSecurity.ps1 $LocalGroups = "Distributed COM Users","Performance Monitor Users" if ($add){ $LocalGroups | %{Set-UserLocalGroup -Computer $ComputerName -Group $_ -Domain $DomainName -User $UserName -add} Set-WMINamespaceSecurity root/CIMv2 add "$DomainName\$UserName" Enable,MethodExecute,ReadSecurity,RemoteAccess -computer $ComputerName } elseif($remove) { $LocalGroups | %{Set-UserLocalGroup -Computer $ComputerName -Group $_ -Domain $DomainName -User $UserName -remove} Set-WMINamespaceSecurity root/cimv2 delete "$DomainName\$UserName" -computer $ComputerName }
To check that I can get the performance data I can use the powershell cmdlet Get-WMIObject , as you can see in the screendump, in the first test I do not have the permissions set but in the second it works..
Here are the functions
Set-UserLocalGroup
# Add/Remove user from local group # # Niklas Akerlund/Most code from Scriptingguy Blog http://blogs.technet.com/b/heyscriptingguy/archive/2010/08/19/use-powershell-to-add-domain-users-to-a-local-group.aspx Function Set-UserLocalGroup { [cmdletBinding()] Param( [Parameter(Mandatory=$True)] [string]$Computer, [Parameter(Mandatory=$True)] [string]$Group, [Parameter(Mandatory=$True)] [string]$Domain, [Parameter(Mandatory=$True)] [string]$User, [switch]$add, [switch]$remove ) $de = [ADSI]"WinNT://$Computer/$Group,group" if($add){ $de.psbase.Invoke("Add",([ADSI]"WinNT://$Domain/$User").path) } elseif ($remove){ $de.psbase.Invoke("Remove",([ADSI]"WinNT://$Domain/$User").path) } }
Set-WmiNamespaceSecurity, there was an error in the original code that I have corrected based on the comments, this error caused the function to always run on the local computer instead of the remote when using the -computer parameter.
# Copyright (c) Microsoft Corporation. All rights reserved. # For personal use only. Provided AS IS and WITH ALL FAULTS. # Set-WmiNamespaceSecurity.ps1 # Example: Set-WmiNamespaceSecurity root/cimv2 add steve Enable,RemoteAccess Function Set-WmiNamespaceSecurity { Param ( [parameter(Mandatory=$true,Position=0)][string] $namespace, [parameter(Mandatory=$true,Position=1)][string] $operation, [parameter(Mandatory=$true,Position=2)][string] $account, [parameter(Position=3)][string[]] $permissions = $null, [bool] $allowInherit = $false, [bool] $deny = $false, [string] $computer = ".", [System.Management.Automation.PSCredential] $credential = $null) Process { $ErrorActionPreference = "Stop" Function Get-AccessMaskFromPermission($permissions) { $WBEM_ENABLE = 1 $WBEM_METHOD_EXECUTE = 2 $WBEM_FULL_WRITE_REP = 4 $WBEM_PARTIAL_WRITE_REP = 8 $WBEM_WRITE_PROVIDER = 0x10 $WBEM_REMOTE_ACCESS = 0x20 $WBEM_RIGHT_SUBSCRIBE = 0x40 $WBEM_RIGHT_PUBLISH = 0x80 $READ_CONTROL = 0x20000 $WRITE_DAC = 0x40000 $WBEM_RIGHTS_FLAGS = $WBEM_ENABLE,$WBEM_METHOD_EXECUTE,$WBEM_FULL_WRITE_REP,<code> $WBEM_PARTIAL_WRITE_REP,$WBEM_WRITE_PROVIDER,$WBEM_REMOTE_ACCESS,</code> $READ_CONTROL,$WRITE_DAC $WBEM_RIGHTS_STRINGS = "Enable","MethodExecute","FullWrite","PartialWrite",<code> "ProviderWrite","RemoteAccess","ReadSecurity","WriteSecurity" $permissionTable = @{} for ($i = 0; $i -lt $WBEM_RIGHTS_FLAGS.Length; $i++) { $permissionTable.Add($WBEM_RIGHTS_STRINGS[$i].ToLower(), $WBEM_RIGHTS_FLAGS[$i]) } $accessMask = 0 foreach ($permission in $permissions) { if (-not $permissionTable.ContainsKey($permission.ToLower())) { throw "Unknown permission: $permission</code>nValid permissions: $($permissionTable.Keys)" } $accessMask += $permissionTable[$permission.ToLower()] } $accessMask } if ($PSBoundParameters.ContainsKey("Credential")) { $remoteparams = @{ComputerName=$computer;Credential=$credential} } else { $remoteparams = @{ComputerName=$computerName} } $invokeparams = @{Namespace=$namespace;Path="__systemsecurity=@"} + $remoteParams $output = Invoke-WmiMethod @invokeparams -Name GetSecurityDescriptor if ($output.ReturnValue -ne 0) { throw "GetSecurityDescriptor failed: $($output.ReturnValue)" } $acl = $output.Descriptor $OBJECT_INHERIT_ACE_FLAG = 0x1 $CONTAINER_INHERIT_ACE_FLAG = 0x2 $computerName = (Get-WmiObject @remoteparams Win32_ComputerSystem).Name if ($account.Contains('\')) { $domainaccount = $account.Split('\') $domain = $domainaccount[0] if (($domain -eq ".") -or ($domain -eq "BUILTIN")) { $domain = $computerName } $accountname = $domainaccount[1] } elseif ($account.Contains('@')) { $domainaccount = $account.Split('@') $domain = $domainaccount[1].Split('.')[0] $accountname = $domainaccount[0] } else { $domain = $computerName $accountname = $account } $getparams = @{Class="Win32_Account";Filter="Domain='$domain' and Name='$accountname'"} $win32account = Get-WmiObject @getparams if ($win32account -eq $null) { throw "Account was not found: $account" } switch ($operation) { "add" { if ($permissions -eq $null) { throw "-Permissions must be specified for an add operation" } $accessMask = Get-AccessMaskFromPermission($permissions) $ace = (New-Object System.Management.ManagementClass("win32_Ace")).CreateInstance() $ace.AccessMask = $accessMask if ($allowInherit) { $ace.AceFlags = $OBJECT_INHERIT_ACE_FLAG + $CONTAINER_INHERIT_ACE_FLAG } else { $ace.AceFlags = 0 } $trustee = (New-Object System.Management.ManagementClass("win32_Trustee")).CreateInstance() $trustee.SidString = $win32account.Sid $ace.Trustee = $trustee $ACCESS_ALLOWED_ACE_TYPE = 0x0 $ACCESS_DENIED_ACE_TYPE = 0x1 if ($deny) { $ace.AceType = $ACCESS_DENIED_ACE_TYPE } else { $ace.AceType = $ACCESS_ALLOWED_ACE_TYPE } $acl.DACL += $ace.psobject.immediateBaseObject } "delete" { if ($permissions -ne $null) { throw "Permissions cannot be specified for a delete operation" } [System.Management.ManagementBaseObject[]]$newDACL = @() foreach ($ace in $acl.DACL) { if ($ace.Trustee.SidString -ne $win32account.Sid) { $newDACL += $ace.psobject.immediateBaseObject } } $acl.DACL = $newDACL.psobject.immediateBaseObject } default { throw "Unknown operation: $operation`nAllowed operations: add delete" } } $setparams = @{Name="SetSecurityDescriptor";ArgumentList=$acl.psobject.immediateBaseObject} + $invokeParams $output = Invoke-WmiMethod @setparams if ($output.ReturnValue -ne 0) { throw "SetSecurityDescriptor failed: $($output.ReturnValue)" } } }
Comments
Hello, and great script.
Question:
How would I add permissions to “This namespace and subnamespaces”?
From what I can tell this will only add permissions to the parent namespace given as the param.
Thanks in advance
James
Nice! I want to try this out, but it’s a pain to copy paste, since it includes all 158 line numbers. Is there a downloadable file for that script?
Jim (or others w/the same issue), if you mouse over the code, a button appears in the upper-right that allows you to copy the code without line numbers.
@Jim
Jim (or others w/the same issue), if you mouse over the code, a button appears in the upper-right that allows you to copy the code without line numbers.
Thanks. I came up with a rewrite that suites my needs pretty well.
# Create Permissions for non-admin user on remote computers
#
# Niklas Akerlund / 2012-08-22
# http://vniklas.djungeln.se/2012/08/22/set-up-non-admin-account-to-access-wmi-and-performance-data-remotely-with-powershell/
# Edward Skarke modifications 2014-11-29
Param (
[switch]$add,
[switch]$remove,
$ComputerName = $env:Computername,
$UserName = (Write-Error "UserName is required")
)
# Add/Remove user from local group
#
# Niklas Akerlund/Most code from Scriptingguy Blog http://blogs.technet.com/b/heyscriptingguy/archive/2010/08/19/use-powershell-to-add-domain-users-to-a-local-group.aspx
if (-not $add -and -not $remove)
{
Write-Error "-add or -remove is required."
}
$ScriptPath = Split-Path $MyInvocation.MyCommand.Definition
# add functions to call
. "$ScriptPath\Function_Set-UserLocalGroup.ps1"
. "$ScriptPath\Function_Set-WmiNamespaceSecurity.ps1"
$UsernameArr = $Username -split ',(?=(?:[^"]|"[^"]*")*$)' -split ';(?=(?:[^"]|"[^"]*")*$)'
$UsernameArr | %{
if ($Username -match "\\")
{
$Username = ($_ -split "\\")[1]
$DomainName = ($_ -split "\\")[0]
}
elseif ($Username -match "@")
{
$Username = ($_ -split "@")[0]
$DomainName = ($_ -split "@")[1]
}
else
{
$Username = $_
$DomainName = $env:Computername
}
$LocalGroups = "Distributed COM Users","Performance Monitor Users"
if ($add){
$LocalGroups | %{Set-UserLocalGroup -Computer $ComputerName -Group $_ -Domain $DomainName -User $UserName -add}
Set-WMINamespaceSecurity root/CIMv2 add "$DomainName\$UserName" Enable,MethodExecute,ReadSecurity,RemoteAccess -computer $ComputerName
} elseif($remove) {
$LocalGroups | %{Set-UserLocalGroup -Computer $ComputerName -Group $_ -Domain $DomainName -User $UserName -remove}
Set-WMINamespaceSecurity root/cimv2 delete "$DomainName\$UserName" -computer $ComputerName
}
}
I have the same question as James.
Hello, and great script.
Question:
How would I add permissions to “This namespace and subnamespaces”?
From what I can tell this will only add permissions to the parent namespace given as the param.
Thanks in advance
James
Great scripts. Here’s another rewrite that adds a lot more validation logic (had to remove the “remove” functionality for my needs but it could be easily added back)
https://github.com/JustinGrote/Scripts/blob/master/Set-WMIReadOnlyAccess.ps1
I am a newbie at powershell, when I run your script I get the following message…Any thoughts
WARNING: Exception calling “add” with “1” argument(s): “The specified account name is already a member of the group.
I am sure the account I am adding has never been added to that machine
Thanks inadvance
BTW, This is on Justin Grote’s script
Thanks
There is a bug in row 059 of Set-WmiNamespaceSecurity
$remoteparams = @{ComputerName=$computerName} should be $remoteparams = @{ComputerName=$computer}
To add the permission to “This namespace and subnamespaces”, just set the second bit flag (value 0x2) in the ACE flags of the DACL and windows will automatically add the permissions to all subnamespaces as well. In this script it is added if the allowInherit parameter is set to true. However, this script will then also set the first bit (value 0x1) in the ACE flags which will result in an error when I test it, so I assume this is a bug. Line 109 of this script should only read
$ace.AceFlags = $CONTAINER_INHERIT_ACE_FLAG
and the automatic inheritance will work 😉