AspAlliance.com LogoASPAlliance: Articles, reviews, and samples for .NET Developers
URL:
http://aspalliance.com/articleViewer.aspx?aId=650&pId=-1
CodeSnip: Impersonation in ThreadPool Worker Threads
page
by J. Ambrose Little
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 31654/ 34

Setting Up Impersonation

This article mainly applies to situations where you are using ASP.NET's built-in identity impersonation feature (without integrated authentication).  By default, ASP.NET will run under the ASPNET account identity.  There are several ways to change this, but perhaps the easiest and most secure is to change the identity of the virtual directory by going to the IIS manager, right-clicking on the applicable virtual directory or site, choosing Properties, clicking on the Directory Security tab, and then choosing Edit.  This will bring up a dialog that will let you control the "anonymous" user for that site or virtual directory.

Ensure that Anonymous access is checked and then choose the account identity you want the application to run under.  By default, this will be IUSR_MACHINENAME, but you can create and use any account for this purpose.  For this article, we'll leave the default, but if you do change the identity to an account you created, be sure it has all of the appropriate permissions.  In IIS 6.0 (Windows Server 2003), it's a simple matter of adding the account to the IIS_WPG group, but it is a little more complex for IIS 5.x (Windows XP/2000).  For more information on this, see this KB article.

So, once you've configured the identity you want, you can close out of IIS manager.  The next thing you need to do is add the following element under the system.web element in your application's web.config (or you can do this in machine.config to configure impersonation for all applications, as some hosters do).

<identity impersonate="true" />

That should be it, your application should now run under the identity you configured in the first step for the virtual directory or site.  In our case, the application is now running under IUSR_MACHINENAME.

Sharing Identity Between Threads

Now that the easy, or rather, intuitive part is out of the way, let's get to the meat of our problem.  The problem we're addressing is that when you are running under an impersonated identity as we set up on the previous page, that identity will not be shared by threads in the ThreadPool.  So if you are using the ThreadPool to do a little asynchronous programming, you will likely want to share the impersonated identity that your application is running under with the ThreadPool; otherwise, you may get permission denied errors because the ThreadPool will run under the default ASP.NET process identity, which is the ASPNET account, by default.

Sharing the impersonated identity, I was glad to find, was actually easier than it sounds.  First, you need to create a static (Shared in VB.NET) member of type System.Security.Principal.WindowsIdentity.  You could just add this member to any ol' web form class, but it would make it easiest to just add it to your global class in the Global.asax.cs/vb file as below.
[C#]

public class Global : HttpApplication
{
  internal static System.Security.Principal.WindowsIdentity ApplicationIdentity;

  protected void Application_Start(Object sender, EventArgs e)
  {
    ApplicationIdentity = 
      System.Security.Principal.WindowsIdentity.GetCurrent();
  }
}

[VB.NET]
Public Class Global 
  Inherits System.Web.HttpApplication

  Shared Friend ApplicationIdentity As System.Security.Principal.WindowsIdentity

  Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
    ApplicationIdentity = _
      System.Security.Principal.WindowsIdentity.GetCurrent()
  End Sub
End Class

The static/Shared keyword means that the member will be shared between multiple threads, which is exactly what we're after.  But that's not all we have to do.  You'll note that we also need to set that identity, which we can do in almost any place in the application, but again, it makes most sense to do it in the Global class, so we just set it to be the current identity of the application. 

The next step is to actually do some asynchronous work in which we'll need to use this.  For this article, I created a web form, added a Label and a Button to it, and then added the following code.

private void Button1_Click(object sender, System.EventArgs e)
{
  this.Label1.Text += "Main Thread Identity: " + 
    Global.ApplicationIdentity.Name + "<br />";
  System.Threading.ThreadPool.QueueUserWorkItem(
    new System.Threading.WaitCallback(this.DoSomething));
  System.Threading.Thread.Sleep(2000);
}

private void DoSomething(object blah)
{
  this.Label1.Text += "Worker Thread Identity Pre-Impersonation: " + 
    System.Security.Principal.WindowsIdentity.GetCurrent().Name + "<br />";
  this.TryFileAccess();
  System.Security.Principal.WindowsImpersonationContext wi = 
    Global.ApplicationIdentity.Impersonate();
  this.Label1.Text += "Worker Thread Identity During Impersonation: " + 
    System.Security.Principal.WindowsIdentity.GetCurrent().Name + "<br />";
  this.TryFileAccess();
  wi.Undo();
  this.Label1.Text += "Worker Thread Identity Post-Impersonation: " + 
    System.Security.Principal.WindowsIdentity.GetCurrent().Name + "<br />";
}

The key things to note here are that we need to create a new WindowsPrincipal object using our shared application identity, then set the current thread's CurrentPrincipal object to be our new principal, and lastly, we need to call Impersonate on our shared identity.  Once this is done, the worker thread will be impersonating the same identity that our application uses.

You may also note that I called Undo on the impersonation context.  You'll probably want to do this in order to restore the worker thread's identity.  I haven't tested, but since the threads are pooled, if you change the identity and release them back to the pool without undoing the impersonation, I imagine it'd keep that identity, which may cause unexpected results.  Also, the call to Thread.Sleep is simply there to ensure that our worker thread has time to execute before the page is rendered and cleaned up--you normally would not want to put that in your asynchronous code (it kind of defeats the purpose).

Now, originally, I just tested by printing out the name of the current identity, but I wanted to take it a step further and ensure that the impersonation was actually taking place as expected, so I added a Test.txt file to my application's root and set the security permissions on it to explicitly deny access to the ASPNET identity.  You can do this by simply removing the Users group's access to the file, assuming ASPNET is not in any other groups that have access and is not explicitly granted access.

I then created the following simple method to try to open the file and read its contents.

private void TryFileAccess()
{
 System.IO.StreamReader sr = null;
 try
 {
  sr = System.IO.File.OpenText(Server.MapPath(this.Request.ApplicationPath + "/Test.txt"));
  this.Label1.Text += "File says: " + sr.ReadToEnd() + "<br />";
 }
 catch (Exception ex)
 {
  this.Label1.Text += "Error reading file: " + ex.ToString() + "<br />";
 }
 finally
 {
  if (sr != null)
   sr.Close();
 }
}

As you can see, it is fairly straightforward; I just open a file using a StreamReader and read the contents of the file as a string, appending it onto my Label control for display.  I catch any exceptions and print them to the screen using the same approach.  And finally, I ensure the file gets closed. 

As an aside, you should always use a finally statement when accessing files to ensure they get closed properly.  In C#, you can also use the using statement,  but if you need to handle exceptions generated from the code, it's best to stick with the try-catch-finally.

So, running this code produced the following results for me:
Main Thread Identity: GRENDEL\IUSR_GRENDEL
Worker Thread Identity Pre-Impersonation: GRENDEL\ASPNET
Error reading file: System.UnauthorizedAccessException: Access to the path "H:\Projects\SomeSolution\SomeWeb\Test.txt" is denied. at System.IO.__Error.WinIOError(Int32 errorCode, String str) at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, Boolean useAsync, String msgPath, Boolean bFromProxy) at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize) at System.IO.StreamReader..ctor(String path, Encoding encoding, Boolean detectEncodingFromByteOrderMarks, Int32 bufferSize) at System.IO.StreamReader..ctor(String path) at System.IO.File.OpenText(String path) at SomeWeb.WebForm1.TryFileAccess() in h:\Projects\SomeSolution\SomeWeb\webform1.aspx.cs:line 80
Worker Thread Identity During Impersonation: GRENDEL\IUSR_GRENDEL
File says: Blah blah blah
Worker Thread Identity Post-Impersonation: GRENDEL\ASPNET

You can see by this that it does indeed work as expected:  When running under the default/non-impersonated identity of the worker thread (ASPNET), I get an access denied error trying to read my file.  But after I impersonate the shared application identity, I'm able to access the file as expected.  Also note that after calling Undo, it does in fact revert to the default process identity.

Summing It Up

If you've gotten this far, you've seen both how to share an impersonated identity with worker threads, and you've seen an example that demonstrates it works.  But in case you just skipped here or got lost in the midst of it all, here are the steps you need to take:

  1. Set up your application to run under a specific identity in IIS.
  2. Add the <identity impersonate="true" /> element to the system.web section of your Web.config (or Machine.config) file.
  3. Create a static/Shared member of type System.Security.Principal.WindowsIdentity.
  4. Set that shared member to be the identity of your main application threads.
  5. Do some asynchronous calling using ThreadPool.QueueUserWorkItem.
  6. In your worker thread method(s), create a new WindowsPrincipal using the shared identity from Step 3.
  7. Set the current thread's principal to that principal.
  8. Call Impersonate on the shared identity.
  9. Do your work using that identity.
  10. Call Undo on your impersonation context.

That's it.  In ten, relatively easy steps, you can ensure that all of your application's code is running under the same identity configured by you (or your hosting provider). 

In fact, to make your life easier, I've created a reusable class that you can plug into your application.  This class cuts out steps 3, 4, 6, and 7--nearly half.  All you need to do is download (includes C# and VB.NET version) the code and either drop the (language-appropriate) file into your project or copy and paste the class into your Global class.  If you do the latter, you can then simply call like below.

System.Security.Principal.WindowsImpersonationContext wi = Global.Impersonation.Impersonate();
// do some stuff
wi.Undo()

Again, calling Undo is optional, but it is recommended.  If you look at the class, it you will note that I get around step 4 (above) by creating a static property that will get the current thread's identity.  It is virtually guaranteed this will work as long as the first thread to call Impersonate is running under the desired identity, which should be the case most of the time.  If you want to be 100% sure, you can always still just set that property to the identity that you want prior to calling Impersonate.

Please note this article came out of a real need caused by just this situation with dasBlog and my hosting provider.  If you're curious, you can read about that on my blog.  Also note that I translated the impersonation helper class using Alex Lowe's translator.  It worked like a charm except it didn't understand my in-class #region statements.  It also didn't handle my delegate instantiation in my test code--I had to change it to use AddressOf Me.DoSomething, but it still saved me a lot of time!


Product Spotlight
Product Spotlight 

©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-03-29 11:06:48 AM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search