HOWTO: Using PowerShell in ASP.NET (.NET Framework 2.0)

HOWTO: Using PowerShell in ASP.NET (.NET Framework 2.0)

Rate This
  • Comments 12

The easiest solution is to create a COM component and register that component with component services (COM+) running the component under a specific user identity. Why do I say this? Read on...

In my case the Web site/Application was configured to run under the DefaultAppPool (Identity = Network Service) and you I wanted to use PowerShell to Enable a Mailbox on Exchange 2007. The code is running on the Exchange 2007 Server itself. What are the available options?

1) Impersonate the user who has the required permissions to Enable a Mailbox in code.
2) Create a new Application Pool, configure it's Identity to a user who has the required permissions to Enable a Mailbox.
3) Create a COM component and register that component with component services(COM+) running the component under a user who has the required permissions to Enable a Mailbox.

Lets discuss each of these options:

1) Impersonate the user who has the required permissions to Enable a Mailbox in code

This is how the ASP.NET code looks like. Thanks to Dan for the code.

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Text;
using System.Security;
using System.Security.Principal;
using System.Runtime.InteropServices;

public partial class _Default : System.Web.UI.Page
{
    [DllImport("advapi32.dll", SetLastError = true)]
    public static extern bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);

    // Holds the new impersonation token.
    private IntPtr _userToken = IntPtr.Zero; 
    private WindowsImpersonationContext _impersonationContext = null;
    
   private void EnableMailbox(string domain, string domainController)
    {

        RunspaceConfiguration config = RunspaceConfiguration.Create();
        PSSnapInException warning;
       
        // Load Exchange PowerShell snap-in.
        config.AddPSSnapIn("Microsoft.Exchange.Management.PowerShell.Admin", out warning);
        if (warning != null) throw warning;

        // This is where be begin the Impersonation
        BeginImpersonation(domain);
       
        using (Runspace thisRunspace = RunspaceFactory.CreateRunspace(config))
        {
            try
            {
                thisRunspace.Open();
                using (Pipeline thisPipeline = thisRunspace.CreatePipeline())
                {
                    //Please change parameter values.
                    thisPipeline.Commands.Add("Enable-Mailbox");
                    thisPipeline.Commands[0].Parameters.Add("Identity", @"DOM157711\xxxxxxx");
                    thisPipeline.Commands[0].Parameters.Add("Database", @"XXXXXXX-8\First Storage Group\Mailbox Database");
                    // Need the line below when impersonating. KB943937. Need rollup 1 for sp1.
                    thisPipeline.Commands[0].Parameters.Add("DomainController", domainController); 

                    try
                    {
                        thisPipeline.Invoke();
                    }
                    catch (Exception exx)
                    {
                        Response.Write("Error: " + exx.ToString());
                    }

                    // Check for errors in the pipeline and throw an exception if necessary.
                    if (thisPipeline.Error != null && thisPipeline.Error.Count > 0)
                    {
                        StringBuilder pipelineError = new StringBuilder();
                        pipelineError.AppendFormat("Error calling Enable-Mailbox.");
                        foreach (object item in thisPipeline.Error.ReadToEnd())
                        {
                            pipelineError.AppendFormat("{0}\n", item.ToString());
                        }

                        throw new Exception(pipelineError.ToString());
                    }
                }
            }

            finally
            {
                thisRunspace.Close();
                EndImpersonation();
            }
        }

    }

    private void BeginImpersonation(string domain)
    {
        //Please change the User Name and Password
        string UserName = "UserName";
        string Password = "Password";

        EndImpersonation();

        Response.Write("User Before Impersonation: " + WindowsIdentity.GetCurrent().Name + "</BR>");
                
        bool success = LogonUser(
                UserName,
                domain,
                Password,
                2,
                0,
                ref _userToken);

        // Did it work?
        if (!success) throw new Exception(string.Format("LogonUser returned error {0}", System.Runtime.InteropServices.Marshal.GetLastWin32Error()));

        WindowsIdentity fakeId = new WindowsIdentity(_userToken);
        _impersonationContext = fakeId.Impersonate();
        Response.Write("User After Impersonation: " + WindowsIdentity.GetCurrent().Name + "</BR>");

    }
    
    private void EndImpersonation()
    {
        if (_impersonationContext != null)
        {
            try
            {
                _impersonationContext.Undo();
            }
            finally
            {
                _impersonationContext.Dispose();
                _impersonationContext = null;
            }
        }
    }

    protected void Button1_Click(object sender, EventArgs e)
    {
        // Use the current domain and domain controller. The domain controller parameter is needed for Impersonation to work
        // Please change ualues to suit you needs.
        string domain = "DOM157711";
        string domainController = "XXXXX-8";
        try
        {
            EnableMailbox(domain, domainController);
        }
        catch (Exception ex)
        {
            Response.Write(ex.ToString());
        }
        finally
        {
            Response.Write("Done..");
        }

    }
}

This is how my Web.Config looks like:

<?xml version="1.0"?>
<configuration>
    <appSettings/>
    <connectionStrings/>
    <system.web>
        <compilation debug="false">
            <assemblies>
              <add assembly="System.Management.Automation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
            </assemblies>
         </compilation>
        <authentication mode="None"/>
    </system.web>
</configuration>

This code works and does what I want! Now I decided to move this code off the Exchange 2007 server and deploy it on to a Windows 2003 box that has the Exchange 2007 Management tools(SP1 and RU 5) Installed. When I did this it no longer worked! I get an exception instead:

Error: Microsoft.Exchange.Configuration.Tasks.ThrowTerminatingErrorException: Database "XXXXXXX-8\First Storage Group\Mailbox Database" was not found. Please make sure you have typed it correctly. ---> Microsoft.Exchange.Configuration.Tasks.ManagementObjectNotFoundException: Database "XXXXXXX-8\First Storage Group\Mailbox Database" was not found. Please make sure you have typed it correctly. at Microsoft.Exchange.Configuration.Tasks.DataAccessTask`1.GetDataObject[TObject](IIdentityParameter id, IConfigDataProvider session, ObjectId rootID, Nullable`1 notFoundError, LocalizedString multipleFoundError) at Microsoft.Exchange.Management.RecipientTasks.EnableMailbox.InternalBeginProcessing() at Microsoft.Exchange.Configuration.Tasks.Task.BeginProcessing() --- End of inner exception stack trace --- at Microsoft.Exchange.Configuration.Tasks.Task.ThrowTerminatingError(Exception exception, ErrorCategory category, Object target) at Microsoft.Exchange.Configuration.Tasks.Task.ProcessUnhandledException(Exception e) at Microsoft.Exchange.Configuration.Tasks.Task.BeginProcessing() at System.Management.Automation.Cmdlet.DoBeginProcessing() at System.Management.Automation.CommandProcessorBase.DoBegin()

When I alter the same code to run in a Windows Application, it works. This for sure has something to do with Impersonation and IIS being involved. Not going into details of why I got the error I decided to look for alternatives as I was running short on time.

2) Create a new Application Pool, configure it's Identity to a user who has the required permissions to Enable a Mailbox

The simplest way I could get my code to work from the Windows 2003 box was to create a new Application Pool in IIS and configure its Identity to a user who had permissions to Enable a mailbox. Now this leaves me with a very very big security hole, since my entire application is running under an account which is quite powerful, I can only imagine the risks.

If you can guarantee that security is taken care of, you could use this approach but I personally would never use it. having said that, what else could I do? I do not want to run my code on the Exchange 2007 Server.

3) Create a COM component and register that component with component services(COM+) running the component under a user who has the required permissions to Enable a Mailbox.

This is the only approach that worked for me in this case and also in the past. Here is how you can get started:

Creating the C# Component
Build and configure a COM+ component to host the PowerShell code and ensure that it is running with the credentials of a service account and not inheriting any credentials from the caller.

Creating the project…
1)Start Visual Studio 2005
2)Select File->New->Project…
3)Under C#, select Windows
4)In the “Template Types” select “Class Library”
5)In the Name box type “PowerShellComponent
6)Click OK.
7)Add references to System.EnterpriseServices, System.Management.Automation(browse to the C:\Program Files\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0\).

Signing the Assembly...
1)Right click on the PowerShellComponent project in the Solution Explorer and click properties. In the signing tab check the box to “Sign the assembly” and create a new strong name key called “PowerShellComponent.snk”.
2)Still in the project properties window, select the Application tab and click “Assembly Information…”, check the box that says, “Make assembly COM-Visible”.
3)Open the AssemblyInfo.cs and add “using System.EnterpriseServices;” to the top and the following lines at the bottom of the file…

[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationName("PowerShellComponent")]
[assembly: Description("Simple PowerShell Component Sample")]
[assembly: ApplicationAccessControl(
           false,
           AccessChecksLevel = AccessChecksLevelOption.Application,
           Authentication = AuthenticationOption.None,
           ImpersonationLevel = ImpersonationLevelOption.Identify)]

Add the ManagementCommands class…
1)Select the “Solution Explorer” view tab.
   Rename the class1.cs file to “ManagementCommands.cs”.
2)Remove the code generated by the Wizard.
   Cut and paste the code provided below:

using System;
using System.Collections.Generic;
using System.Text;
using System.Management.Automation.Runspaces;
using System.EnterpriseServices;
using System.Security;
using System.Security.Principal;
using System.Runtime.InteropServices;

namespace PowerShellComponent
{
    public class ManagementCommands : System.EnterpriseServices.ServicedComponent
    {
        public String EnableMailbox(string domain, string domainController)
        {
            String ErrorText="";
            RunspaceConfiguration config = RunspaceConfiguration.Create();
            PSSnapInException warning;

            // Load Exchange PowerShell snap-in.
            config.AddPSSnapIn("Microsoft.Exchange.Management.PowerShell.Admin", out warning);
            if (warning != null) throw warning;

            using (Runspace thisRunspace = RunspaceFactory.CreateRunspace(config))
            {
                try
                {
                    thisRunspace.Open();
                    using (Pipeline thisPipeline = thisRunspace.CreatePipeline())
                    {
                        //Please change parameter values.
                        thisPipeline.Commands.Add("Enable-Mailbox");
                        thisPipeline.Commands[0].Parameters.Add("Identity", @"DOM157711\xxxxxxx");
                        thisPipeline.Commands[0].Parameters.Add("Database", @"XXXXXXX-8\First Storage Group\Mailbox Database");
                        // Need the line below when impersonating. KB943937. Need rollup 1 for sp1.
                        thisPipeline.Commands[0].Parameters.Add("DomainController", domainController);

                        try
                        {
                            thisPipeline.Invoke();
                        }
                        catch (Exception ex)
                        {
                            ErrorText = "Error: " + ex.ToString();
                        }

                        // Check for errors in the pipeline and throw an exception if necessary.
                        if (thisPipeline.Error != null && thisPipeline.Error.Count > 0)
                        {
                            StringBuilder pipelineError = new StringBuilder();
                            pipelineError.AppendFormat("Error calling Enable-Mailbox.");
                            foreach (object item in thisPipeline.Error.ReadToEnd())
                            {
                                pipelineError.AppendFormat("{0}\n", item.ToString());
                            }

                            ErrorText = ErrorText + "Error: " + pipelineError.ToString();
                        }
                    }
                }

                finally
                {
                    thisRunspace.Close();
                    EndImpersonation();
                }
            }
            if (ErrorText == "")
                return "Success";
            else
                return ErrorText;
        }
        public string GetIdentity()
        {
            AppDomain.CurrentDomain.SetPrincipalPolicy(System.Security.Principal.PrincipalPolicy.WindowsPrincipal);

            System.Security.Principal.WindowsPrincipal user = System.Threading.Thread.CurrentPrincipal  as System.Security.Principal.WindowsPrincipal;

            return user.Identity.Name;
        }
    }
}

Create and Configure the COM+ Component

1. Build the PowerShellComponent project in Visual Studio
2. From the Visual Studio command prompt run “regsvcs PowerShellComponent.dll”
3. Open “Component Services” from “Administrative Tools”
4. Under “Component Services”, “Computers”, “My Computer”, “COM+ Applications”, find “PowerShellComponent” and right click on it then select “Properties”
5. On the identity tab:
    a. Configure “This user” as an account that has Exchange Admin privileges on your Exchange server.
6. Click OK

Code for the Web Application(ASP.Net) to Consume PowerShellComponent

Below is the code that needs to be pasted in the Code Behind file(This code assumes we are using Default.aspx.cs)

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Text;
using System.Security;
using System.Security.Principal;
using System.Runtime.InteropServices;

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        // Use the current domain and domain controller
        // Please change ualues to suit you needs.
        string domain = "DOM157711";
        string domainController = "XXXXXXX-8";
        try
        {
            EnableMailbox(domain, domainController);
        }
        catch (Exception ex)
        {
            Response.Write(ex.ToString());
        }
        finally
        {
            Response.Write("Done..");
        }
    }
   private void EnableMailbox(string domain, string domainController)
   {
       Response.Write(string.Format("<b>Web Application Context:</b> {0}<br>", WindowsIdentity.GetCurrent().Name));

       // Create the COM+ component
       PowerShellComponent.ManagementCommands objManage = new PowerShellComponent.ManagementCommands();

       String Results;
       Results = objManage.EnableMailbox(domain1, domainController1);
       Response.Write(Results);

       // Print out the security context of the component
       Response.Write(string.Format("<br><b>Component Context:</b> {0}<br><br>", objManage.GetIdentity()));

       objManage = null;

   }
}

Copy PowerShellComponent.dll from the output directory of the PowerShellComponent project to the “bin” under you Web Application and build the Web Project and test.

Talking a look at the results

When you access this page from another machine as different user or anonymous user you will notice that the web application and the COM+ component are running in different security contexts. For example in my test, I access the page as anonymous user and when I hit the web I got the following result…

Web Application Context: NT AUTHORITY\NETWORK SERVICE
Success
Component Context: DOM157711\akashb

The key here is that Web Application is running under NT AUTHORITY\NETWORK SERVICE and has no permission to Enable a Mailbox. The web form makes the call to our COM+ component which always executes the PowerShellComponent code in the context of account that we configured in COM+, no matter who the calling process is running as.

Using the COM+ approach has always worked for me, it also help you avoid other issues as outlined by Matt in his post.

Enjoy!

Leave a Comment
  • Please add 7 and 8 and type the answer here:
  • Post
  • Thanks for this tutorial, it's exactly what I am looking for.  I have a question regarding the 3rd option.  Are these two lines copied and used exactly as provided?

    thisPipeline.Commands[0].Parameters.Add("Identity", @"DOM157711\xxxxxxx");

    thisPipeline.Commands[0].Parameters.Add("Database", @"XXXXXXX-8\First Storage Group\Mailbox Database");

    Are the Identity and Database strings retrieved from a varaible elsewhere?

  • thisPipeline.Commands[0].Parameters.Add("Identity", @"DOM157711\xxxxxxx");

    In the above line "Identity" is the name of the parameter and "DOM157711\xxxxxxx" is the value. The parameter name will always remain the same and the values will change.

    The same is true for the next line. "Database" is the name of the parameter and "XXXXXXX-8\First Storage Group\Mailbox Database" is the value.

    The list of parameters for the Enable-Mailbox cmdlet can be found in the article below:

    http://technet.microsoft.com/en-us/library/aa998251.aspx

  • Will this also work to create a mailbox?

  • Yes, In that case the command and the parameters would change.

    The line:

    thisPipeline.Commands.Add("Enable-Mailbox");

    would change to:

    thisPipeline.Commands.Add("Create-Mailbox");

    and you will need to alter the parameters accordingly.

  • Thank you for your time answering my questions.  This is the first time I attempted this.  We are running this on a virtural 64 bit Windows 2k3 server.  I am getting an error stating "No Windows PowerShell Snap-ins are available for version 1".  From what I could find elsewhere on the web, this has to do with the bit version.  Can this code be modified to load the 64 bit version of PowerShell?  Again, thank you so much for your time and your quick responses!

  • If you just build the .net application for x64, you should be fine.

  • Hi,

    I followed step by step for Step 3.  However when I run ASP.net page I am getting the error below with Visual Studio 2008 (remote machine with PowerShell installed):

    System.Management.Automation.PSArgumentException: No snap-ins have been registered for Windows PowerShell version 2

    Please help.  Do I need to install exchange Management Tools in Visual Studio Remote machine?

    Thanks.

  • If you are connecting to Exchange 2007 then yes. For Exchange 2010 you can use Remote powershell with Powershell 2.0.

  • Akash,

    This has been very helpful over the past year.  Thank you for your time.  Will this also work to delete or remove a mailbox permanently?  Would it be just "thisPipeline.Commands.Add("Remove-Mailbox");"?

    Thank you!

  • Yes, you will also need to send in the required parameters for the Remove-Mailbox cmdlet.

  • Thank you again for your time and a great post.

  • Background:

    To get information on EAS activity on an Exchange server, you will need to call Exchange

Page 1 of 1 (12 items)