简体   繁体   中英

TPL and Impersonation

I am using the Impersonator class (see http://www.codeproject.com/KB/cs/zetaimpersonator.aspx ) to switch the user context at runtime.

At the same time, i am now restructuring my program from a single threaded design to multi-threaded one (using TPL / mainly the Task type, that is).

As this impersonation is something that is happening with native API functions on a thread level, i was wondering how far TPL is compatible with it. If i change the user context inside a task, is that user context still set if the task is finished and the thread returns to the ThreadPool? Will other tasks started inside this task implicitly use that context?

I tried to find out by myself with unit testing, and my deduction from the first unit test:

  • Tasks started inside a thread while impersonated "magically" inherit the user context.
  • The inherited impersonation is not revoked when the origin task/thread does its Impersonation.Undo().

The second unit test shows that if the impersonation is not explicitly revoked, the user context "survives" on the thread returning to the thread pool, and other following tasks may now be randomly run in different user contexts, depending on the thread they are assigned to.

My question: Is there a better way to realize impersonation than via native API calls? Maybe one that is more focused on TPL and bound to a task instead of a thread? If there is a change to mitigate the risk of executing tasks in a random context i would gladly do it...

These are the 2 unit tests i wrote. You will have to modify the code slightly to use your own mechanism for receiving user credentials if you want to run the tests yourself, and the log4net calls are surely easily removed.

Yeah, i know, Thread.Sleep() is bad style, i am guilty of having been lazy there... ;-)

  private string RetrieveIdentityUser()
  {
     var windowsIdentity = WindowsIdentity.GetCurrent();
     if (windowsIdentity != null)
     {
        return windowsIdentity.Name;
     }
     return null;
  }

  [TestMethod]
  [TestCategory("LocalTest")]
  public void ThreadIdentityInheritanceTest()
  {
     string user;
     string pw;
     Security.Decode(CredentialsIdentifier, out user, out pw);

     string userInMainThread = RetrieveIdentityUser();
     string userInTask1BeforeImpersonation = null;
     string userInTask1AfterImpersonation = null;
     string userInTask2 = null;
     string userInTask3 = null;
     string userInTask2AfterImpersonationUndo = null;

     var threadlock = new object();
     lock (threadlock)
     {
        new Task(
           () =>
           {
              userInTask1BeforeImpersonation = RetrieveIdentityUser();
              using (new Impersonator(user, Domain, pw))
              {
                 userInTask1AfterImpersonation = RetrieveIdentityUser();
                 lock (threadlock)
                 {
                    Monitor.Pulse(threadlock);
                 }
                 new Task(() =>
                 {
                    userInTask2 = RetrieveIdentityUser();
                    Thread.Sleep(200);
                    userInTask2AfterImpersonationUndo = RetrieveIdentityUser();
                 }).Start();
                 Thread.Sleep(100);
              }
           }).Start();

        Monitor.Wait(threadlock);
        RetrieveIdentityUser();
        new Task(() => { userInTask3 = RetrieveIdentityUser(); }).Start();
        Thread.Sleep(300);

        Assert.IsNotNull(userInMainThread);
        Assert.IsNotNull(userInTask1BeforeImpersonation);
        Assert.IsNotNull(userInTask1AfterImpersonation);
        Assert.IsNotNull(userInTask2);
        Assert.IsNotNull(userInTask3);

        // context in both threads equal before impersonation
        Assert.AreEqual(userInMainThread, userInTask1BeforeImpersonation);

        // context has changed in task1
        Assert.AreNotEqual(userInTask1BeforeImpersonation, userInTask1AfterImpersonation);

        // impersonation to the expected user
        Assert.AreEqual(Domain + "\\" + user, userInTask1AfterImpersonation);

        // impersonation is inherited
        Assert.AreEqual(userInTask1AfterImpersonation, userInTask2);

        // a newly started task from the main thread still shows original user context
        Assert.AreEqual(userInMainThread, userInTask3);

        // inherited impersonation is not revoked
        Assert.AreEqual(userInTask2, userInTask2AfterImpersonationUndo);
     }
  }

  [TestMethod]
  [TestCategory("LocalTest")]
  public void TaskImpersonationTest()
  {
     int tasksToRun = 100; // must be more than the minimum thread count in ThreadPool
     string userInMainThread = RetrieveIdentityUser();
     var countdownEvent = new CountdownEvent(tasksToRun);
     var exceptions = new List<Exception>();
     object threadLock = new object();
     string user;
     string pw;
     Security.Decode(CredentialsIdentifier, out user, out pw);
     for (int i = 0; i < tasksToRun; i++)
     {
        new Task(() =>
        {
           try
           {
              try
              {
                 Logger.DebugFormat("Executing task {0} on thread {1}...", Task.CurrentId, Thread.CurrentThread.GetHashCode());
                 Assert.AreEqual(userInMainThread, RetrieveIdentityUser());
                 //explicitly not disposing impersonator / reverting impersonation
                 //to see if a thread reused by TPL has its user context reset
                 // ReSharper disable once UnusedVariable
                 var impersonator = new Impersonator(user, Domain, pw);
                 Assert.AreEqual(Domain + "\\" + user, RetrieveIdentityUser());
              }
              catch (Exception e)
              {
                 lock (threadLock)
                 {
                    var newException = new Exception(string.Format("Task {0} on Thread {1}: {2}", Task.CurrentId, Thread.CurrentThread.GetHashCode(), e.Message));
                    exceptions.Add(newException);
                    Logger.Error(newException);
                 }
              }
           }
           finally
           {
              countdownEvent.Signal();
           }
        }).Start();
     }
     if (!countdownEvent.Wait(TimeSpan.FromSeconds(5)))
     {
        throw new TimeoutException();
     }
     Assert.IsTrue(exceptions.Any());
     Assert.AreEqual(typeof(AssertFailedException), exceptions.First().InnerException.GetType());
  }

}

The WindowsIdentity (which is changed through impersonation) is stored in a SecurityContext . You can determine how this impersonation "flows" in various ways . Since you mentioned, you are using p/invoke, note the caveat in the SecurityContext documentation :

The common language runtime (CLR) is aware of impersonation operations performed using only managed code, not of impersonation performed outside of managed code, such as through platform invoke...

However, I'm not entirely certain that the impersonation is really inherited from one task to the other here, or whether the behavior you are observing is due to task inlining. Under certain circumstances, a new task might execute synchronously on the same thread-pool thread. You can find an excellent discussion of this here .

Nonetheless, if you want to make sure that certain tasks are always running under impersonation, while others don't, may I suggest looking into custom Task Schedulers. There is documentation on MSDN on how to write your own , including code-samples for a few common types of schedulers that you can use as a starting point.

Since impersonation is a per-thread setting, you could have your own task scheduler that keeps around one thread (or a few threads) that are running under impersonation when they execute tasks. This could also reduce the number of times you have to switch in and out of impersonation when you have many small units of work.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM