Powershell: PSCredential Parameterbinding quirks

Created on 7 Aug 2017  路  10Comments  路  Source: PowerShell/PowerShell

Hi,

I'm trying to create a parameter class that can be directly bound to a PSCredential parameter.
A simple example use of the objective:

$cred = [DbaCredential]"foo\bar"
Get-WmiObject -ComputerName "computer1" -Credential $cred -Class "Win32_OperatingSystem"

This parameter class is basically a functionality wrapper around the PSCredential object (So I didn't try to reinvent security. Not good enough for that). When using it like this however I am rewarded by a seriously awesome message in red:
image

Now, this parameter class is for module-internal use only, so I suppose I could just call it like this:

$cred = [DbaCredential]"foo\bar"
Get-WmiObject -ComputerName "computer1" -Credential $cred.Credential -Class "Win32_OperatingSystem"

Which would work just fine. My queen and project leader however would appreciate to not confuse random contributors (because we already have a lot of fancy features and we don't want to scare them away. They are trying to help after all). Every little thing adds up and so I've spent a few hours trying to make it work.
By now I really want to know what the heck is happening here, out of sheer bloody-mindedness!

What I've tried so far:

  • Set up implicit conversion
  • Implemented IConvertible
  • Created a PSTypeConverter for the conversion and attached it to the input type.

Guess what ... none of it worked.
Giving up on it, I dug down into the exception and PowerShell code (well ... it was more a parallel thing, alternating between the two):

  • The parameter binding passes it to the CredentialAttribute's Transform method. This in return calls LanguagePrimitives's T FromObjectAs<T> method to have it bake a PSCredential object. In that method it tries to cast to PSCredential _which should be working, dangit!_ , returns null instead and the Transform method throws the exception which is then relayed by the parameter binding.

What is happening here and what did I forget? Any ideas?

The current implementation can be checked out on our dbatools project's DbaCredentialParameter branch

Thanks in advance for any ideas, inspiration or suggestions,
Cheers,
Fred

Issue-Question WG-Engine

All 10 comments

In case you don't strictly need an actual _.NET_ type, here's a simple all-PowerShell alternative using PowerShell's ETS (extended type system):

  • Define a function New-DbaCredential that creates [PSCredential] instances that are ad-hoc-decorated with the additional members you needed and assign them custom _PS_ type name DbaCredential.

  • The resulting instances will still behave like [PSCredential] in the context of cmdlet calls, yet you can access the extra members as needed; Get-Member will report the made-up type name DbaCredential.


function New-DbaCredential {

  param([string] $UserName = (whoami), $Foo, $Bar)

  # Construct a [PSCredential] instance...
  # Note: This will prompt for a password.
  $cred = Get-Credential -UserName $UserName -Message 'Enter password:'

  # Now shamelessy lie about the output object's type...
  $cred.pstypenames.Insert(0, 'DbaCredential')

  # ... and decorate it with your custom properties and output the result.
  Add-Member -InputObject $cred  -PassThru -NotePropertyMembers @{ Foo = $Foo; Bar = $Bar }

}

Now you can construct instances of your custom "type":

> $custCred = New-DbaCredential -UserName jdoe -Foo f -Bar b; $custCred

Foo Bar UserName                     Password
--- --- --------                     --------
f   b   jdoe     System.Security.SecureString
> $custCred.pstypenames
DbaCredential
System.Management.Automation.PSCredential
System.Object
> $custCred -is [PSCredential]
True

Heya, thanks for the input.
Indeed, that would do for adding some new members on a limited scale.

Design goal:

  • Add input validation that gets inherited through
  • Avoid any changes in handling internally
  • Eliminate credential prompts on older PowerShell versions, without an eternal chain of if/else
  • Flexibly convert input of specified types without blanket-accepting everything

So basically, all we want to tell our contributors is Instead of a [PSCredential], make it a [DbaCredential] parameter and the rest will take care of itself.

However - sorry - it really is beside the point for the purpose of this question. Because for me it really is about what is happening here on parameter binding. Maybe it is a bug. Maybe I am missing something. I want to know, in order to further my understanding of how parameter binding works.

Without debugging, I'm guessing the problem is here - LanguagePrimitives.FromObjectAs is essentially the C# as operator, but this method also unwraps a PSObject.

This means your implicit conversion operator will not help, the transformation is converting your value to null. The fix might be as simple as using LanguagePrimitives.TryConvertTo<PSCredential> instead.

@FriedrichWeinmann: Got it, thanks for the feedback (on my somewhat self-indulgent workaround).

Kudos for a sophisticated approach.

The link to your source doesn't seem to work, but this one does.

Heya @lzybkr ,
thanks for the pointer, though it's more this position that had me confounded. That string wouldn't work was kinda expected, but in the call from line 53 it tries to coerce it into a PSCredential object by casting it - which bloody darned well should be working, as far as I can see.

Just did a short code extraction of it though, in order to test things out:

Add-Type @"
 using System;
 using System.Management.Automation;

 public static class test
 {
     public static PSCredential FromObjectAs(Object castObject)
         {
             PSCredential returnType = default(PSCredential);

             // First, see if we can cast the direct type
             PSObject wrapperObject = castObject as PSObject;
             if (wrapperObject == null)
             {
                 returnType = (PSCredential)castObject;
             }
             // Then, see if it is an PSObject wrapping the object
             else
             {
                 returnType = (PSCredential)wrapperObject.BaseObject;
             }

             return returnType;
         }
 }
 "@
[test]::FromObjectAs($cred)

And it definitely is throwing an invalid cast exception.
I just don't see why ...

@FriedrichWeinmann - in your example, your implicit operator is not called because the static type of the parameter is object. Something like this should work:

C# returnType = (PSCredential)(castObject as DbaCredentialParameter)

Of course this can't work in PowerShell because PowerShell won't statically know your type.

argh. Alright and good to know, thanks for the info.
So basically, in order for it to work, I'd have to add that operator to Object itself? Somehow I doubt the system makes that possible ... :(
But thank you very much for explaining that. Doesn't look like there's much I can do then (Still gonna poke around a bit when I've got the time).

Alright, did some poking around:

  • Cannot extend class to add operators when it's not in my own library
  • Cannot extend PSCredential, as it is sealed (whyever. I'm sure that made sense to some people, can't for the heck of me see it though)
  • Cannot undo the credential attribute

Ergo: Mission impossible.

While I'm somewhat disgruntled over the entire parameter binding/type coercion system being ignored for pscredentials, just to force credential prompt (which can be implemented in a different way btw - at least our prompt works just fine as well and we don't mess with parameter attributes), there is little I can do about it. Given that ... well, time to call it a day and spend my time on something with a better effort/success ratio ;)

Cheers,
Fred

Your questioned is answered - but there are changes we can make to the engine to enable your scenario - it won't help with Windows PowerShell, but going forward it might be helpful, right?

Indeed you could and in the long term it would be helpful.
Basically, supporting consistency in the type coercion process would help. It's extremely flexible and powerful, so hardcoding for a specific input type is not helpful as far as I can see it, so yes, I'd appreciate it. I don't really know how much impact 'fixing' this would have compared to the effort to do it - I use parameter classes heavily, but I don't know anybody else who does so - but even then it would make powershell behavior more uniform and consistent.

Btw, I solved the prompt-for-credential issue with a constructor that accepts string. So when you just pass a name, it'll prompt as part of the default coercion process while binding the parameter (and I added a "remember" checkbox, so it'll remember the credential matched to the name for the duration of the process).

The link above is dead, but that's because the branch was merged into development. So if you want to check out our implementation, just download it and run [DbaCredential]"foo" (or build a dummy function to actually have it operate as parameter type).

Was this page helpful?
0 / 5 - 0 ratings