Some time back I wrote a couple of scripts (tokenGroupsDump.vbs and tokenGroupsLogon.vbs) for myself and a customer using the idea of the tokenGroups attribute to access a user or computers group membership without the hassle of having to programmatically do the work especially with all the problems of looking up the primary group ID.
As an exercise I decided to convert the scripts into PowerShell partly to see what would be involved as they were more than a few lines and I could see some real time savings especially in the formatting of the dump script and some interesting learning opportunities in using the WSH components for drive mapping etc.
What I had not expected was to spend the afternoon figuratively banging my head of the wall and spending a considerable part of it reading MSDN documentation without once opening Visual Studio.
As no doubt all of you are aware PowerShell is a .NET console application designed to take full advantage of the .NET framework and is routinely talked of as a replacement for CMD, VBScript etc. In deed most people learning PowerShell quickly come across various examples using [ADSI] to access Active Directory and of invocation of COM objects such as Internet Explorer to open web pages so when they see VBScript such as ShowUserName.vbs below the assumption will be fairly similar to mine that it will take a couple of minutes to convert.
Set oADSystemInfo = CreateObject("ADSystemInfo")
WScript.Echo oADSystemInfo.UserName
You could not be further from the truth if you tried - nearly four hours of digging around PowerShell, MSDN and the internet.
A minutes quick typing produced the following and a confused feeling when also trying the tab completion showed none of the expected methods or properties exposed by ADSystemInfo I was intrigued and honestly a bit pissed this was going to take more than the expected couple of minutes as I had a lot more lines to convert.
PS P:\> $oADSystemInfo = new-object -COM ADSystemInfo
PS P:\> $oADSystemInfo.UserName
PS P:\>
OK back to PowerShell basics and the eponymous Get-Member command which I honestly hoped would show me something but had the sinking feeling before I even hit return. Nothing none of the ever so useful domain, site or user information.
PS P:\> Get-Member -InputObject $oADSystemInfo
TypeName: System.__ComObject
Name MemberType Definition
---- ---------- ----------
CreateObjRef Method System.Runtime.Remoting.ObjRef CreateObjRef(Type
requestedType)
Equals Method System.Boolean Equals(Object obj)
GetHashCode Method System.Int32 GetHashCode()
GetLifetimeService Method System.Object GetLifetimeService()
GetType Method System.Type GetType()
InitializeLifetimeService Method System.Object InitializeLifetimeService()
ToString Method System.String ToString()
PS P:\>
Maybe I had the wrong COM name or PowerShell was somehow mangling the one I typed so .GetType() to see what was being returned in case I how one of the annoying problems from the wrappers like PSBASE introduced. OK now I saw the problem PowerShell is not picking up any of the type info for some reason.
PS P:\> $oADSystemInfo.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False __ComObject System.MarshalByRefObject
PS P:\>
Have a look at the following comparison of the .Psobject output from creating $ie for the Internet Explorer Application object
and the .Psobject from our $oADSystemInfo object that is being the problem.
Key field is the last one TypeNames where $ie is returning the COM GUID d30c1661-cdaf-11d0-8a3e-00c04fc9e26e
but the creation of the $oADSystemInfo by New-Object is not similarly
populating the required information.
PS P:\> $ie = New-Object -COM internetexplorer.application
PS P:\> $ie.Psobject
Members : {Application, Parent, Container, Document...}
Properties : {Application, Parent, Container, Document...}
Methods : {GoBack, GoForward, GoHome, GoSearch...}
ImmediateBaseObject : System.__ComObject
BaseObject : System.__ComObject
TypeNames : {System.__ComObject#{d30c1661-cdaf-11d0-8a3e-00c04fc9e26e}, System.__ComObject, System.MarshalByRefObject, System.Object}
PS P:\> $oADSystemInfo.Psobject
Members : {GetLifetimeService, InitializeLifetimeService, CreateObjRef, GetType...}
Properties : {}
Methods : {GetLifetimeService, InitializeLifetimeService, CreateObjRef, GetType...}
ImmediateBaseObject : System.__ComObject
BaseObject : System.__ComObject
TypeNames : {System.__ComObject, System.MarshalByRefObject, System.Object}
PS P:\>
Now this rings a bell with some problems I had a few years back doing some VB.NET programming and ended up solving using the reflection classes in .NET to get at the 'buried' methods and properties rather than creating an interop library chose to make the calls directly as I wanted to keep the distribution simple and single file. The method to call is InvokeMember from the .NET Type routines.
The call is .InvokeMember(<name>,<invokeAttr>,<binder>,<target>,<args>) where
We can access .InvokeMember from [System.__ComObject] and as long as we know the name and any arguments required for methods we can now make the call directly to the COM object bypassing the layer default layer PowerShell and .NET provide.
<binder> is normally either [System.Reflection.BindingFlags]::GetProperty or
[System.Reflection.BindingFlags]::InvokeMethod.
So we can do the following to return the required username information once $oADSystemInfo has been created.
PS P:\> [System.__ComObject].InvokeMember("UserName",[System.Reflection.BindingFlags]::GetProperty,$null,$oADSystemInfo,$null)
CN=Scotty McLeod,OU=Users,OU=Home,DC=foxnet,DC=local
PS P:\>
Why do we need to jump through these hoops? Because this COM object like
quite a few others has no type library defined and no interop class that
wrappers the calls and makes accessing from .NET systems such as PowerShell
more straightforward. Other solutions to using the above would be to use tlbimp.exe
create a runtime callable wrapper and then do a [Reflection.Assembly]::LoadWithPartialName
to get access to the methods and properties with tab completion etc.