I have a custom function that accepts a [scriptblock] parameter, The scriptblock is serailzed using [System.Management.Automation.PSSerializer]::Serialize() before it is sent to a remote process to be deserialized and invoked. The remote process does not have access to the local variables. I would like to allow the variables to be placed into the scriptblock on function call, however i would settle with a second parameter $ArgumentList to pass the arguments\parameters to the scriptblock. I browsed through System.Management.Automation.dll at Microsoft.PowerShell.Commands.InvokeCommandCommand to see if i could determine how Invoke-command adds this functionality but i'm a beginner in C# and could't figure it out.
How can I expand the variables within the scriptblock before they are sent along their way?
An example of the local serialize function:
Function Send-Command
{
param(
[scriptblock]$Scriptblock
)
[String]$Serialized = [System.Management.Automation.PSSerializer]::Serialize($Scriptblock)
[byte[]]$MessageBytes = [System.Text.Encoding]::UTF8.GetBytes($Serialized)
return $MessageBytes
}
And on the other end:
[byte[]]$Serialized = [System.Text.Encoding]::UTF8.GetString($MessageBytes)
[String]$Deserialized = [System.Management.Automation.PSSerializer]::Deserialize($Serialized.ToString())
return $Deserialized
I call the function with local variables:
$String = "This is a Test"
Send-Command -Scriptblock {Write-Output $String }
And on the other end:
Command: Write-Output $String
Should Output:
Command: Write-Output "This is a Test"
Using PowerShell I would suggest te following:
Why don't you use Invoke-Command with the scriptblock to run the scriptblock on the remote computer? You can combine that both with the $using:LocalVariableName statement to access local variables from the remote host or with local variable expansion as described above, or even combine both.
I guess something like this should do the job:
$ScriptBlock = {
Write-Output $using:Text
}
$Text = 'This is a test'
function Send-Command {
param (
[scriptblock] $ScriptBlock,
[string] $ComputerName,
[System.Management.Automation.PSCredential] $Credential
)
Invoke-Command -ComputerName $Computername -ScriptBlock $ScriptBlock -Credential $Credential
}
$ComputerName = 'Server0123'
$Credential = Get-Credential -Message "Enter credential for computer $($ComputerName)"
Send-Command $ScriptBlock $ComputerName $Credential
PowerShell: Invoke-Command
PowerShell: about_remote_variables
There's a way to deserialize a ScriptBlock.
You have to first create a scriptblock from the string.
Then you have to serialize all the variables that are bounded to the scope.
After it we use a regular expression to replace the variables with a payload to deserialize their values.
When the code is deserialized, it will have in the ScriptBlock an injected code to deserialize the value of the variables, so they retain the contents.
Then you need to call GetNewClosure() to unbound variables from the deserializer scope (any parameters for example), then they will be free to be bounded in the next call.
This solution is based on what the powershell DSC infraestructure does.
Function Serialize-Command
{
param(
[scriptblock]$Scriptblock
)
$rxp = '\$using:(?<var>\w+)'
$ssb = $Scriptblock.ToString()
$cb = {
$v = (Get-Variable -Name $args[0].Groups['var'] -ValueOnly)
$ser = [System.Management.Automation.PSSerializer]::Serialize($v)
"`$([System.Management.Automation.PSSerializer]::Deserialize('{0}'))" -f $ser
}
$sb = [RegEx]::Replace($ssb, $rxp, $cb, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
$Serialized = [System.Management.Automation.PSSerializer]::Serialize($sb)
[System.Text.Encoding]::UTF8.GetBytes($Serialized)
}
Function Deserialize-Command
{
param(
[byte[]]$ScriptblockString
)
$Serialized = [System.Text.Encoding]::UTF8.GetString($ScriptblockString)
$sb = [System.Management.Automation.PSSerializer]::Deserialize($Serialized.ToString())
[Scriptblock]::Create($sb).GetNewClosure()
}
$a = "is this what you wanted?"
$b = Serialize-Command { "test $using:a" }
$a = "okay"
$c = Deserialize-Command $b
$a = "right"
PS> & $c
is this what you wanted?
sources:
https://www.briantist.com/how-to/use-duplicate-dsc-script-resources-in-loop/
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.scriptblock.getnewclosure?view=powershellsdk-1.1.0#System_Management_Automation_ScriptBlock_GetNewClosure
https://devblogs.microsoft.com/powershell/get-closure-with-getnewclosure/
The three common ways to expands variables are the below:
$VariableName
"$VariableName"
$($VariableName)
... but there are other considerations depending on what you are doing.
Related
Trying to use a powershell script with a function as follows:
function MoveCompressFiles{
Param
(
[Parameter(Mandatory=$true )]
[string] $Des,
[Parameter(Mandatory=$true)]
[string] $Src
)
Add-Type -AssemblyName System.Drawing
$files = Get-ChildItem $Src
foreach ($f in $files) {
if (($f.Length / 1KB) -lt [int32]200) {
Copy-Item -Path $f.FullName -Destination $Des
}
else {
Copy-Item -Path $f.FullName -Destination $Des
while (((Get-Item (($Des).ToString() + "\$f")).Length / 1KB ) -gt 500) {
$img = [System.Drawing.Image]::FromFile((($Des).ToString() + "$f"))
[int32]$new_width = $img.Width * (20 / 100);
[int32]$new_height = $img.Height * (20 / 100);
$img2 = New-Object System.Drawing.Bitmap($new_width, $new_height)
$graph = [System.Drawing.Graphics]::FromImage($img2)
$graph.DrawImage($img, 0, 0, $new_width, $new_height)
$newImgName = "M".ToString() + $f.ToString()
$img2.Save(($Des).ToString()+"\$newImgName")
$img.Dispose()
$img2.Dispose()
Remove-Item ($Des.ToString()+$f)
Rename-Item -Path ($Des.ToString()+$newImgName) -NewName "$f"
Write-Host ((Get-Item ($Des.ToString()+$f)).Length / 1KB )
}
$filesize = $f.Length * 0.8
$filesize=($filesize / 1KB)
#$filesize = [math]::round(($filesize / 1KB), 0)
$abc = "KB"
$filesizeSTR = $filesize.ToString() + $abc
Push-Location $Src
mogrify -path $Des -define jpeg:extent=$filesizeSTR $f
Pop-Location
Write-Host "Moved file $f"
}
}
}
Works in Powershell, however when i try to do it it in my solution,
private static void Powershell()
{
string SCRIPT_PATH = System.IO.File.ReadAllText(#"C:\Untitled2.ps1");
using (Runspace runspace = RunspaceFactory.CreateRunspace())
{
runspace.Open();
PowerShell ps = PowerShell.Create();
ps.Runspace = runspace;
ps.AddScript(SCRIPT_PATH);
ps.Invoke();
ps.AddCommand("MoveCompressFiles").AddParameters(new Dictionary<string, string>
{
{"Des" , #"C:\Des"},
{"Src", #"C:\Src"}
});
}
}
It doesn't work, I've tried some other methods of calling a function from a ps script but it still fails to even move the files to another location
Since you need to dot-source your script file (. <script>) in order to make the MoveCompressFiles function available, which requires an .AddScript() call,
I suggest constructing a single piece of PowerShell code in a string variable that both dot-sources the script and invokes your function via a single .AddScript() call.
However, in order to guarantee that .AddScript() works, you must first ensure that the PowerShell execution policy allows script invocation, using a call to Set-ExecutionPolicy; the code below uses -Scope Process, so as to limit the change to the current process.
Update: There's a simpler way to configure the execution policy, via the initial session state - see this answer.
var SCRIPT_PATH = #"C:\Untitled2.ps1";
var src = #"C:\Src";
var des = #"C:\Des";
var script = $#". ""{SCRIPT_PATH}""; MoveCompressFiles -Des ""{des}"" -Src ""{src}""";
using (PowerShell ps = PowerShell.Create())
{
// Make sure that script execution is allowed.
ps.AddCommand("Set-ExecutionPolicy")
.AddParameter("Scope", "Process")
.AddParameter("ExecutionPolicy", "Bypass")
.AddParameter("Force", true);
ps.Invoke();
// Add the PowerShell code constructed above and invoke it.
ps.AddScript(script);
// Use foreach (var o in ps.Invoke()) { Console.WriteLine(o); } to print the output.
ps.Invoke();
}
Note the simplified, implicit runspace creation, by using PowerShell.Create() only.
The embedded PowerShell code dot-sources your script file (. <script>) in order to define the MoveCompressFiles function, and then invokes the function.
Note that the above, as your own code, doesn't capture or print the output from the PowerShell code (.Invoke()'s output).
To see if errors occurred, you can check ps.HadErrors and examine ps.Streams.Error or any of the other streams, such as .ps.Streams.Information for the Write-Host output (the success stream's output is what .Invoke() returns directly).
For instance, use something like the following to print all errors (messages only) that occurred to the console's standard error stream:
foreach (var o in ps.Streams.Error) {
Console.Error.WriteLine(o);
}
As for what you tried:
ps.AddScript(SCRIPT_PATH); ps.Invoke();
While this executes your script, it does so in a child scope, so the embedded function MoveCompressFiles definition is not added to your session's top-level scope, so the subsequent .AddCommand() call fails, because the MoveCompressFiles function isn't available.
Instead, you must dot-source your script (. <script>), which makes it run in the caller's scope and therefore makes its function definition available there.
As an aside: Despite the .AddScript() method's name, its primary purpose is to execute a piece of PowerShell code, not a script file.
To execute the latter (without dot-sourcing), use .AddCommand().
I wrote a PowerShell script to download files using FTPto my local machine.
After the file is downloaded, I want to delete it from the FTP server. I wrote this code too. But unfortunately it's not working.
Can anyone help me to point out what is wrong with my code? Any clues will be helpful ...
Here is my code
function Delete-File($Source,$Target,$UserName,$Password)
{
$ftprequest = [System.Net.FtpWebRequest]::create($Source)
$ftprequest.Credentials = New-Object System.Net.NetworkCredential($UserName,$Password)
if(Test-Path $Source)
{
"ABCDEF File exists on ftp server."
$ftprequest.Method = [System.Net.WebRequestMethods+Ftp]::DeleteFile
$ftprequest.GetResponse()
"ABCDEF File deleted."
}
}
function Get-FTPFile ($Source,$Target,$UserName,$Password)
{
# Create a FTPWebRequest object to handle the connection to the ftp server
$ftprequest = [System.Net.FtpWebRequest]::create($Source)
# set the request's network credentials for an authenticated connection
$ftprequest.Credentials =
New-Object System.Net.NetworkCredential($username,$password)
if(Test-Path $targetpath)
{
"ABCDEF File exists"
}
else
{
"ABCDEF File downloaded"
$ftprequest.Method = [System.Net.WebRequestMethods+Ftp]::DownloadFile
$ftprequest.UseBinary = $true
$ftprequest.KeepAlive = $false
Delete-File $sourceuri $targetpath $user $pass
}
# send the ftp request to the server
$ftpresponse = $ftprequest.GetResponse()
# get a download stream from the server response
$responsestream = $ftpresponse.GetResponseStream()
# create the target file on the local system and the download buffer
$targetfile = New-Object IO.FileStream ($Target,[IO.FileMode]::Create)
[byte[]]$readbuffer = New-Object byte[] 1024
# loop through the download stream and send the data to the target
file
do{
$readlength = $responsestream.Read($readbuffer,0,1024)
$targetfile.Write($readbuffer,0,$readlength)
}
while ($readlength -ne 0)
$targetfile.close()
}
$sourceuri = "ftp://ftpxyz.com/vit/ABCDEF.XML"
$targetpath = "C:\Temp\M\NEWFOLDER\ABCDEF.XML"
$user = "*******"
$pass = "*******"
Get-FTPFile $sourceuri $targetpath $user $pass
Delete-File $sourceuri $targetpath $user $pass
Every time I execute this script, the only statement I get
ABCDEF file downloaded
or
ABCDEF file exists
I guess Delete-File is not executing at all... any type of clue will be helpful.
You cannot use Test-Path with an FTP URL. So your code for deleting the file will never execute.
Just remove the Test-Path condition and try to delete the file unconditionally. Then check for error and treat "file not exist" error as you like.
$ftprequest = [System.Net.FtpWebRequest]::create($Source)
$ftprequest.Credentials =
New-Object System.Net.NetworkCredential($UserName, $Password)
try
{
$ftprequest.Method = [System.Net.WebRequestMethods+Ftp]::DeleteFile
$ftprequest.GetResponse() | Out-Null
Write-Host ("File {0} deleted." -f $Source)
}
catch
{
if ($_.Exception.InnerException.Response.StatusCode -eq 550)
{
Write-Host ("File {0} does not exist." -f $Source)
}
else
{
Write-Host $_.Exception.Message
}
}
Though as you try to delete the file only after you successfully download it, it's actually unlikely that the file won't exist.
So you may consider to give up on any specific error handling.
I ran your script locally to try it out and found a few issues. I refactored also a few things just to make it a bit more readable (at least in my opinion :) ).
Issues
Line 13. $Source parameter there is a ftp://... path. Test-Path will always return $false here and the delete request will never be executed.
In Get-FTPFile you were not referencing the input parameter of the function, instead the variables defined outside of it. I don't know if this was just a copy & paste bug or on purpose. In my opinion you should use the parameters you sent to the function. Lines 38, 39 and 50 at least in my code below.
Code
function Delete-File
{
param(
[string]$Source,
[string]$Target,
[string]$UserName,
[string]$Password
)
$ftprequest = [System.Net.FtpWebRequest]::create($Source)
$ftprequest.Credentials = New-Object System.Net.NetworkCredential($UserName,$Password)
if(Test-Path $Source)
{
"ABCDEF File exists on ftp server."
$ftprequest.Method = [System.Net.WebRequestMethods+Ftp]::DeleteFile
$ftprequest.GetResponse()
"ABCDEF File deleted."
}
}
function Get-FTPFile
{
param(
[string]$Source,
[string]$Target,
[string]$UserName,
[string]$Password
)
# Create a FTPWebRequest object to handle the connection to the ftp server
$ftprequest = [System.Net.FtpWebRequest]::create($Source)
# set the request's network credentials for an authenticated connection
$ftprequest.Credentials =
New-Object System.Net.NetworkCredential($UserName,$Password)
if(Test-Path $Target)
{
"ABCDEF File exists"
}
else
{
"ABCDEF File downloaded"
$ftprequest.Method = [System.Net.WebRequestMethods+Ftp]::DownloadFile
$ftprequest.UseBinary = $true
$ftprequest.KeepAlive = $false
Delete-File $Source $Target $UserName $Password
}
# send the ftp request to the server
$ftpresponse = $ftprequest.GetResponse()
# get a download stream from the server response
$responsestream = $ftpresponse.GetResponseStream()
# create the target file on the local system and the download buffer
$targetfile = New-Object IO.FileStream ($Target,[IO.FileMode]::Create)
[byte[]]$readbuffer = New-Object byte[] 1024
# loop through the download stream and send the data to the target
file
do{
$readlength = $responsestream.Read($readbuffer,0,1024)
$targetfile.Write($readbuffer,0,$readlength)
}
while ($readlength -ne 0)
$targetfile.close()
}
$sourceuri = "ftp://ftpxyz.com/vit/ABCDEF.XML"
$targetpath = "C:\Temp\M\NEWFOLDER\ABCDEF.XML"
$user = "*******"
$pass = "*******"
Get-FTPFile $sourceuri $targetpath $user $pass
#Delete-File $sourceuri $targetpath $user $pass
There are also ready made PowerShell cmdlets for talking to FTP/SFTP, no need to create everything from scratch, unless you are required to.
Anyway, for reference, check out e.g.
https://www.powershellgallery.com/packages/PSFTP
https://www.powershellgallery.com/packages/WinSCP
I am using .NET with powershell trying to retrieve result of Get-Acl command of specific AD object. Unfortunately when I run the code from C# code I get 0 result. Also the ThrowIfError is not throwing any error.
Command test01 = new Command("import-module");
test01.Parameters.Add("name", "activedirectory");
session.Commands.AddCommand(test01);
Command test0 = new Command("Set-Location");
test0.Parameters.Add("Path", "AD:");
session.Commands.AddCommand(test0);
Command test1 = new Command("Get-Acl");
test1.Parameters.Add("Path", identity);
session.Commands.AddCommand(test1);
session.AddCommand("select-object");
session.AddParameter("Property", "Access");
var tempResults1 = session.Invoke();
ThrowIfError();
private void ThrowIfError()
{
var errors = session.Streams.Error;
if (errors.Count > 0)
{
var ex = errors[0].Exception;
session.Streams.ClearStreams();
// Never close session to dispose already running scripts.
throw ex;
}
}
This code running on server in powershell is working correctly:
PS AD:\> Import-Module -Name activedirectory
PS AD:\> set-location ad:
PS AD:\> get-acl -path <distinguishedNameOfADObject>
Question
How to get the same result like I get from Powershell? I should get atleast something not a zero result.
Little background:
I am trying to get Send-As rights not using Get-ADPermission cmdlet because its taking too long time when I need to search for rights within thousands of mailboxes. Using this article link I am trying another approach to get the rights. I have already the slower version working using C# code:
Command command = new Command("Get-ADPermission");
command.Parameters.Add("Identity", identity);
session.Commands.AddCommand(command);
session.AddCommand("where-object");
ScriptBlock filter = ScriptBlock.Create("$_.ExtendedRights -eq 'send-as'");
session.AddParameter("FilterScript", filter);
session.AddCommand("select-object");
session.AddParameter("Property", "User");
tempResults = session.Invoke();
The better way is to define a powershell-script instead of multiple commands to get the values you need. Example with your powershell-code:
using System.Collections.ObjectModel;
using System.DirectoryServices;
using System.Management.Automation;
namespace GetAclPowershellTest
{
class Program
{
static void Main(string[] args)
{
/****Create Powershell-Environment****/
PowerShell PSI = PowerShell.Create();
/****Insert PowershellScript****/
string Content = "param($object); Import-Module ActiveDirectory; Set-Location AD:; Get-ACL -Path $object"; //Add Scrip
PSI.AddScript(Content);
PSI.AddParameter("object", "<distinguishedNameOfADObject>");
/****Run your Script with PSI.Invoke()***/
Collection<PSObject> PSIResults = PSI.Invoke();
/****All Errors****/
Collection<ErrorRecord> Errors = PSI.Streams.Error.ReadAll();
/****needed, because garbagecollector ignores PSI otherwise****/
PSI.Dispose();
/**** Your ACL-Object ****/
ActiveDirectorySecurity MyACL = (ActiveDirectorySecurity)PSIResults[0].BaseObject;
/*insert your code here*/
}
}
}
This example works for me.
You have to set a reference to the Powershell-Assembly (Usually you can find it at "C:\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\3.0\System.Management.Automation.dll")
Benefit of this solution is, you could read a .ps1-File you got from someone, fill the parameters with the objects you have and the script runs like in a standard powershell-session. The only requirement to set parameters is the param-part in the Script.
More Infos about param: https://technet.microsoft.com/en-us/library/jj554301.aspx
Hope, this helps...
Greetings, Ronny
Update:
string Content = "param($object); Import-Module ActiveDirectory; Set-Location AD:; (Get-ACL -Path $object).Access | Where-Object{($_.ActiveDirectoryRights -eq 'ExtendedRight') -and ($_.objectType -eq 'ab721a54-1e2f-11d0-9819-00aa0040529b')}";
And the loop at the end looks like this now:
foreach (PSObject o in PSIResults)
{
ActiveDirectoryAccessRule AccessRule = (ActiveDirectoryAccessRule)o.BaseObject;
/**do something with the AccessRule here**/
}
I'm trying to put something together that will use the AD Thumbnail photo to set a user's account picture on Windows 8. It seems like I should be able to use the API from WinRT to do this. I've pieced something together from various sources about calling the API from powershell, but I can't get it working. Here's an example of what I've tried to do:
$photo = ([ADSISEARCHER]“samaccountname=$($username)”).findone().properties.thumbnailphoto
$path = "C:\temp\Photo.jpg"
$photo | set-content $path -encoding byte
[Windows.System.UserProfile.UserInformation,Windows.System.UserProfile,ContentType=WindowsRuntime] > $null
[Windows.System.UserProfile.UserInformation]::SetAccountPictureAsync($photo)
I've tried a couple of other variations, but no matter what I do, I end up with an error like this:
Cannot convert argument "image", with value: "System.Object[]", for "setAccountPictureAsync" to type "Windows.Storage.IStorageFile" . . .
Is there something simple that I'm missing here to make this work?
I found this blog post by Keith Hill which seems like it might be helpful, but I am not sure if it directly translates to the issue I'm having.
Thanks!
Aurock
https://fleexlab.blogspot.com/2018/02/using-winrts-iasyncoperation-in.html has a pure-PowerShell solution.
Add-Type -AssemblyName System.Runtime.WindowsRuntime
$asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | ? { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' })[0]
function Await($WinRtTask, $ResultType) {
$asTask = $asTaskGeneric.MakeGenericMethod($ResultType)
$netTask = $asTask.Invoke($null, #($WinRtTask))
$netTask.Wait(-1) | Out-Null
$netTask.Result
}
This could then be used as:
$photoPath = "$home\Pictures\Photo.jpg"
$file = Await ([Windows.Storage.StorageFile]::GetFileFromPathAsync($photoPath)) ([Windows.Storage.StorageFile])
$result = Await ([Windows.System.UserProfile.UserInformation]::SetAccountPictureAsync($file)) ([Windows.System.UserProfile.SetAccountPictureResult])
SetAccountPicture expects an object implementing IStorageFile and not a byte array. I would save the picture to your Pictures folder then load that into a StorageFile as shown below. You should be able to pass that object into the SetAccountPicture() method e.g.
$photoPath = "$home\Pictures\Photo.jpg"
$asyncOp = [Windows.Storage.StorageFile]::GetFileFromPathAsync($photoPath)
$typeName = 'PoshWinRT.AsyncOperationWrapper[Windows.Storage.StorageFile]'
$wrapper = new-object $typeName -Arg $asyncOp
$file = $wrapper.AwaitResult()
$asyncOp = [Windows.System.UserProfile.UserInformation]::SetAccountPictureAsync($file)
$typeName = 'PoshWinRT.AsyncOperationWrapper[Windows.System.UserProfile.SetAccountPictureResult]'
$wrapper = new-object $typeName -Arg $asyncOp
$result = $wrapper.AwaitResult()
$wrapper.Dispose()
I am trying to create a Windows Application that will be able to run a variety of Powershell scripts.
I have a script which works as it should (when run from the Powershell prompt), and my Windows Application seems to execute it like it should, but it is unable to find the methods on my OU.
When I execute the script from the Windows Application, I get these messages out:
ERROR: The following exception occurred while retrieving member "Create": "There
is no such object on the server.
"
ERROR: The following exception occurred while retrieving member "Delete": "There
is no such object on the server."
Powershell script:
function New-AdUser {
param (
[string] $Username = $(throw "Parameter -Username [System.String] is required."),
[string] $Password = $(throw "Parameter -Password [System.String] is required."),
[string] $OrganizationalUnit = "Users",
[string] $DisplayName,
[string] $FirstName,
[string] $LastName,
[string] $Initials,
[string] $MobilePhone,
[string] $Description,
[switch] $CannotChangePassword,
[switch] $PasswordNeverExpires,
[switch] $Disabled
)
try {
$currentDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$dn = $currentDomain.GetDirectoryEntry().distinguishedName
$ou = [ADSI] "LDAP://CN=$OrganizationalUnit,$dn"
$userAccount = $ou.Create("user", "cn=$Username")
$userAccount.SetInfo()
$userAccount.userAccountControl = ($userAccount.userAccountControl.Item(0) -bxor 0x0002) #Enable the account
$userAccount.SetInfo()
$userAccount.sAMAccountName = $Username
$userAccount.SetInfo()
$userAccount.userPrincipalName = ("{0}#{1}" -f $Username, $currentDomain.Name)
if ($DisplayName) {
$userAccount.displayName = $DisplayName
}
if ($Description) {
$userAccount.description = $Description
}
if ($FirstName) {
$userAccount.givenName = $FirstName
}
if ($LastName) {
$userAccount.SN = $LastName
}
if ($Initials) {
$userAccount.initials = $Initials
}
if ($MobilePhone) {
$userAccount.mobile = $MobilePhone
}
$userAccount.SetInfo()
$userAccount.SetPassword($Password)
# Password
if ($PasswordNeverExpires) {
$userAccount.userAccountControl = ($userAccount.userAccountControl.Item(0) -bxor 0x10000)
}
if ($CannotChangePassword) {
$everyOne = [System.Security.Principal.SecurityIdentifier]'S-1-1-0'
$EveryoneDeny = new-object System.DirectoryServices.ActiveDirectoryAccessRule ($Everyone,'ExtendedRight','Deny', [System.Guid]'ab721a53-1e2f-11d0-9819-00aa0040529b')
$self = [System.Security.Principal.SecurityIdentifier]'S-1-5-10'
$SelfDeny = new-object System.DirectoryServices.ActiveDirectoryAccessRule ($self,'ExtendedRight','Deny', [System.Guid]'ab721a53-1e2f-11d0-9819-00aa0040529b')
$userAccount.get_ObjectSecurity().AddAccessRule($selfDeny)
$userAccount.get_ObjectSecurity().AddAccessRule($EveryoneDeny)
$userAccount.CommitChanges()
}
$userAccount.SetInfo()
if ($Disabled) {
$userAccount.userAccountControl = ($userAccount.userAccountControl.Item(0) -bxor 0x0002)
}
$userAccount.SetInfo()
} catch {
Write-Error $_
$ou.Delete("user", "cn=$Username")
return $false
}
return $true
}
The C# code I have is this:
PowerShell ps = PowerShell.Create();
ps.AddScript(GetScript("New-AdUser.ps1"));
ps.Invoke();
ps.AddCommand("New-AdUser").AddParameters(
new List<CommandParameter>() {
new CommandParameter("Username", username),
new CommandParameter("Password", password),
new CommandParameter("FirstName", firstName),
new CommandParameter("LastName", lastName),
new CommandParameter("DisplayName", realName),
new CommandParameter("Initials", initials),
new CommandParameter("MobilePhone", mobilePhone),
new CommandParameter("OrganizationalUnit", "Users"),
new CommandParameter("PasswordNeverExpires")
}
);
var results = ps.Invoke();
foreach (var obj in results)
Console.WriteLine(obj.ToString());
if (ps.Streams.Error.Count > 0)
{
foreach (var err in ps.Streams.Error)
Console.WriteLine("ERROR: {0}", err.ToString());
}
Seems that you are just creating a user in AD. By having the c# code calling a powershell script, you are adding another moving part in your script. Why not call it directly in C# code. Check this MSDN article.
The problem appears to be that the Create method on your ADSI object, $ou, doesn't exist. I would check that it is getting created properly. Run the script outside your application to ensure that it works, or have an extra line that displays its members:
$ou | Get-Member
It almost appears as though the Runspace in the application is being created with a restrictive RunspaceConfiguration, so it can't find System.DirectoryServices for the AD functionality you need.
What do you get when you run the following within in your application?
string script = #"[AppDomain]::CurrentDomain.GetAssemblies()";
PowerShell ps = new PowerShell();
ps.AddScript(script);
var output = ps.Invoke();
foreach (var a in output.Select(pso => (System.Reflection.Assembly)pso.BaseObject))
Console.WriteLine("Assembly: " + a.FullName);
When I run that under the debugger in a plain console application I get 28 assemblies (19 outside the debugger), including System.DirectoryServices. The [AppDomain]::CurrentDomain.GetAssemblies() bit shows 16 when I run it on a vanilla command prompt. System.DirectoryServices shows up in all three lists.
When run from within C# I found that I need to add the PowerShell snap-in "Microsoft.Windows.AD" before being able to run the cmdlet's it provides.