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());
}
}