Recently, ok more than 7 or 8 months ago. It has been a while since I last blogged. I was working on a intranet web application and faced with testing role based business rules. I needed to impersonate users temporarily. I needed both positive and negative tests. I needed to package the impersonation code so all developer on our team could easily reuse. I needed to do it over night so team members that needed it could start right away. I already implemented some code in our app for temporary impersonation but it wasn't abstracted to a reusable piece of code. This is how I envisioned the code would look when developers used it. (simplified case)
[Test]
public void Test_A() {
ILocation location = LocationServices.GetLocation("LOCATION_TYPE_A");
Assert.IsNotNull(location);
//Impersonate a non supervisor.
using (IdentityBlock idBloc = new IdentityBlock("HoboJoe")) {
idBloc.ToString(); //just to get rid of warnings.
Assert.IsFalse(location.CanViewPersonnel());
}
//Impersonate an actual Supervisor.
using (IdentityBlock idBloc = new IdentityBlock("JoeCool")) {
idBloc.ToString(); //just to get rid of warnings.
Assert.IsTrue(location.CanViewPersonnel());
}
}
Now I was off to create the IdendityBlock class to abstract the impersonation code away. So I headed home for the night. After all this is when the real creative work starts. If you are going to write Impersonation code then keep Keith Browns book/wiki close by.
This is what I came up with:
public class IdentityBlock : IDisposable {
WindowsImpersonationContext wic;
[DllImport("advapi32.dll", SetLastError=true)]
static extern bool LogonUser(
string principal,
string authority,
string password,
LogonTypes logonType,
LogonProviders logonProvider,
out IntPtr token);
[DllImport("kernel32.dll", SetLastError=true)]
static extern bool CloseHandle(IntPtr handle);
enum LogonTypes : uint {
Interactive = 2,
Network,
Batch,
Service,
NetworkCleartext = 8,
NewCredentials
}
enum LogonProviders : uint {
Default = 0, // default for platform (use this!)
WinNT35, // sends smoke signals to authority
WinNT40, // uses NTLM
WinNT50 // negotiates Kerb or NTLM
}
public IdentityBlock(string user) {
SetThreadIdentity(user);
}
public IdentityBlock(string user, bool impersonateMe) {
if(impersonateMe) {
wic = ImpersonateUser(user);
}
else {
SetThreadIdentity(user);
}
}
~IdentityBlock() {
this.Dispose(false);
}
/// <summary>
/// Set the thread identity
/// </summary>
/// <param name="user"></param>
private void SetThreadIdentity(string user) {
WindowsIdentity id;
string pwd = GetPassword(user); // psuedo code
IntPtr token;
bool result = LogonUser(
user, "[Put_Your_Domain_Here]",
pwd,
LogonTypes.Interactive,
LogonProviders.Default,
out token);
if (result) {
id = new WindowsIdentity(token);
CloseHandle(token);
WindowsPrincipal p = new WindowsPrincipal(id);
System.Threading.Thread.CurrentPrincipal = p;
}
else {
Console.WriteLine("LogonUser failed: {0}",
Marshal.GetLastWin32Error());
throw new Exception(string.Format("LogonUser failed: {0}. User: {1}",
Marshal.GetLastWin32Error(), user));
}
}
/// <summary>
/// Set the thread identity and impersonate.
/// </summary>
private WindowsImpersonationContext ImpersonateUser(string user) {
WindowsIdentity id;
string pwd = GetPassword(user); // psuedo code
IntPtr token;
bool result = LogonUser(
user,
"[Put_Your_Domain_Here]",
pwd,
LogonTypes.Interactive,
LogonProviders.Default,
out token);
if (result) {
id = new WindowsIdentity(token);
CloseHandle(token);
WindowsPrincipal p = new WindowsPrincipal(id);
System.Threading.Thread.CurrentPrincipal = p;
return id.Impersonate();
}
else {
Console.WriteLine("LogonUser failed: {0}",
Marshal.GetLastWin32Error());
throw new Exception(string.Format("LogonUser failed: {0}. User: {1}",
Marshal.GetLastWin32Error(), user));
}
}
#region IDisposable Members
/// <summary>
/// Dispose of this object's resources.
/// </summary>
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(true); // as a service to those who might inherit from us
}
/// <summary>
/// Free the instance variables of this object.
/// </summary>
protected virtual void Dispose(bool disposing) {
if (! disposing)
return; // we're being collected, so let the GC take care of this object
else {
if(wic != null) wic.Undo();
WindowsPrincipal p = new WindowsPrincipal(WindowsIdentity.GetCurrent());
System.Threading.Thread.CurrentPrincipal = p;
}
}
#endregion
}
Note: I supply the user name to the IdentiyBlock constructor. And then in the guts of the the IdentityBlock I am tied to a GetPassword implementation. Rather it be hard coded or a Static helper class. But even in a UnitTest harness you need to figure out a secure method to protecting these Prinicpals. If you are lucky enough to be on a native Windows 2003 AD then you could use Protocal Transition. Maybe I will come back to this some day polish that last piece.
Comments and ideas are welcome.
And I was working with Nunit. I believe that tools like MBUnit may have impersonation abstraction features build in. Don't quote me on that.