Blog Home  Home RSS 2.0 Atom 1.0 CDF  
IdentityCrisis - NUnit and Impersonation Blocks
Code is a commodity. Platform awareness ... Priceless
 
 Sunday, December 11, 2005

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.

12/11/2005 10:48:13 PM (GMT Standard Time, UTC+00:00)  #       | 
Copyright © 2010 Joseph E Shook. All rights reserved.
DasBlog 'Portal' theme by Johnny Hughes.
Pick a theme: