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

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 PowerShellComponentproject 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!